Skip to content

Commit

Permalink
token vault to lock/unlock
Browse files Browse the repository at this point in the history
  • Loading branch information
aalavandhan committed Dec 18, 2020
1 parent c121295 commit 0935b0a
Show file tree
Hide file tree
Showing 2 changed files with 291 additions and 0 deletions.
86 changes: 86 additions & 0 deletions contracts/TokenVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.6.12;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "./bridge-interfaces/IERC20.sol";

/**
* @title TokenVault
* @dev This is a holder contract for Tokens which will be deployed on the 'master' chain (Ethereum).
*
* When a user transfers Tokens from the master chain to another chain
* through a bridge, Tokens are 'locked' in this vault contract
* and 'bridge-secured' Tokens (xc-tokens) are 'mint' on the other chain.
* This vault contract transfers Tokens from the depositor's wallet to itself.
*
* When a user transfers xc-tokens from another chain back to the master chain
* through a chain-bridge instance, xc-tokens are 'burnt' on the other chain
* and locked Tokens are 'unlocked' from this vault contract on the master chain.
* The vault contract transfers Tokens from itself to the recipient's wallet.
*
* The vault owner curates a list of bridge-gateways which are allowed to
* lock and unlock tokens.
*
*/
contract TokenVault is Ownable {
using SafeMath for uint256;

event Locked(address indexed bridgeGateway, address indexed token, address indexed depositor, uint256 amount);

event Unlocked(address indexed bridgeGateway, address indexed token, address indexed recipient, uint256 amount);

event GatewayWhitelistUpdated(address indexed bridgeGateway, bool active);

// White-list of trusted bridge gateway contracts
mapping(address => bool) public whitelistedBridgeGateways;

modifier onlyBridgeGateway() {
require(
whitelistedBridgeGateways[msg.sender],
"TokenVault: Bridge gateway not whitelisted"
);
_;
}

/**
* @notice Adds bridge gateway contract address to whitelist.
* @param bridgeGateway The address of the bridge gateway contract.
*/
function addBridgeGateway(address bridgeGateway) external onlyOwner {
whitelistedBridgeGateways[bridgeGateway] = true;
emit GatewayWhitelistUpdated(bridgeGateway, true);
}

/**
* @notice Removes bridge gateway contract address from whitelist.
* @param bridgeGateway The address of the bridge gateway contract.
*/
function removeBridgeGateway(address bridgeGateway) external onlyOwner {
delete whitelistedBridgeGateways[bridgeGateway];
emit GatewayWhitelistUpdated(bridgeGateway, false);
}

/**
* @notice Transfers specified amount from the depositor's wallet and locks it in the gateway contract.
*/
function lock(address token, address depositor, uint256 amount) external onlyBridgeGateway {
require(IERC20(token).transferFrom(depositor, address(this), amount));
emit Locked(msg.sender, token, depositor, amount);
}

/**
* @notice Unlocks the specified amount from the gateway contract and transfers it to the recipient.
*/
function unlock(address token, address recipient, uint256 amount) external onlyBridgeGateway {
require(IERC20(token).transfer(recipient, amount));
emit Unlocked(msg.sender, token, recipient, amount);
}

/**
* @notice Total token balance secured by the gateway contract.
*/
function totalLocked(address token) public view returns (uint256) {
return IERC20(token).balanceOf(address(this));
}
}
205 changes: 205 additions & 0 deletions test/token_vault.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
const { ethers } = require('@nomiclabs/buidler');
const { expect } = require('chai');

let accounts,
deployer,
deployerAddress,
bridge,
bridgeAddress,
otherBridge,
otherBridgeAddress,
vault,
mockToken;
async function setupContracts () {
accounts = await ethers.getSigners();
deployer = accounts[0];
bridge = accounts[1];
otherBridge = accounts[3];
deployerAddress = await deployer.getAddress();
bridgeAddress = await bridge.getAddress();
otherBridgeAddress = await otherBridge.getAddress();

mockToken = await (await ethers.getContractFactory('MockERC20'))
.connect(deployer)
.deploy('MockToken', 'MOCK');

vault = await (await ethers.getContractFactory('TokenVault'))
.connect(deployer)
.deploy();
}

describe('TokenVault:Initialization', () => {
before('setup TokenVault contract', setupContracts);

it('should set the owner', async function () {
expect(await vault.owner()).to.eq(await deployer.getAddress());
});
});

describe('TokenVault:addBridgeGateway', async () => {
beforeEach('setup TokenVault contract', setupContracts);

it('should NOT be callable by non-owner', async function () {
await expect(
vault.connect(accounts[5]).addBridgeGateway(bridgeAddress),
).to.be.revertedWith('Ownable: caller is not the owner');
});

it('should be callable by owner', async function () {
await expect(vault.connect(deployer).addBridgeGateway(bridgeAddress)).to.not
.be.reverted;
});

it('should add to the whitelist', async function () {
expect(await vault.whitelistedBridgeGateways(bridgeAddress)).to.be.false;
await vault.connect(deployer).addBridgeGateway(bridgeAddress);
expect(await vault.whitelistedBridgeGateways(bridgeAddress)).to.be.true;
});

it('should NOT affect others', async function () {
expect(await vault.whitelistedBridgeGateways(otherBridgeAddress)).to.be
.false;
expect(await vault.whitelistedBridgeGateways(bridgeAddress)).to.be.false;
await vault.connect(deployer).addBridgeGateway(bridgeAddress);
expect(await vault.whitelistedBridgeGateways(bridgeAddress)).to.be.true;
expect(await vault.whitelistedBridgeGateways(otherBridgeAddress)).to.be
.false;
});
});

describe('TokenVault:removeBridgeGateway', async () => {
beforeEach('setup TokenVault contract', async () => {
await setupContracts();
await vault.connect(deployer).addBridgeGateway(bridgeAddress);
await vault.connect(deployer).addBridgeGateway(otherBridgeAddress);
});

it('should NOT be callable by non-owner', async function () {
await expect(
vault.connect(accounts[5]).removeBridgeGateway(bridgeAddress),
).to.be.revertedWith('Ownable: caller is not the owner');
});

it('should be callable by owner', async function () {
await expect(vault.connect(deployer).removeBridgeGateway(bridgeAddress)).to
.not.be.reverted;
});

it('should remove from the whitelist', async function () {
expect(await vault.whitelistedBridgeGateways(bridgeAddress)).to.be.true;
await vault.connect(deployer).removeBridgeGateway(bridgeAddress);
expect(await vault.whitelistedBridgeGateways(bridgeAddress)).to.be.false;
});

it('should NOT affect others', async function () {
expect(await vault.whitelistedBridgeGateways(otherBridgeAddress)).to.be
.true;
expect(await vault.whitelistedBridgeGateways(bridgeAddress)).to.be.true;
await vault.connect(deployer).removeBridgeGateway(bridgeAddress);
expect(await vault.whitelistedBridgeGateways(bridgeAddress)).to.be.false;
expect(await vault.whitelistedBridgeGateways(otherBridgeAddress)).to.be
.true;
});
});

describe('TokenVault:lock:accessControl', async () => {
beforeEach('setup TokenVault contract', async function () {
await setupContracts();
await vault.connect(deployer).addBridgeGateway(bridgeAddress);
await vault.connect(deployer).addBridgeGateway(otherBridgeAddress);
await mockToken.connect(deployer).approve(vault.address, 10000);
});

it('should NOT be callable by non-bridge', async function () {
await expect(
vault.connect(deployer).lock(mockToken.address, deployerAddress, 123),
).to.be.revertedWith('TokenVault: Bridge gateway not whitelisted');
});

it('should be callable by bridge', async function () {
await expect(vault.connect(bridge).lock(mockToken.address, deployerAddress, 123)).to.not.be
.reverted;
await expect(vault.connect(otherBridge).lock(mockToken.address, deployerAddress, 123)).to.not
.be.reverted;
});
});

describe('TokenVault:lock', async () => {
beforeEach('setup TokenVault contract', async function () {
await setupContracts();
await vault.connect(deployer).addBridgeGateway(bridgeAddress);
await mockToken.connect(deployer).approve(vault.address, 999);
});

it('should transfer tokens from the depositor to the contract', async function () {
const _b = await mockToken.balanceOf(deployerAddress);
await expect(vault.connect(bridge).lock(mockToken.address, deployerAddress, 999))
.to.emit(mockToken, 'Transfer')
.withArgs(deployerAddress, vault.address, 999);
const b = await mockToken.balanceOf(deployerAddress);
expect(_b.sub(b)).to.eq(999);
});

it('should update total locked', async function () {
await vault.connect(bridge).lock(mockToken.address, deployerAddress, 999);
expect(await vault.totalLocked(mockToken.address)).to.eq(999);
});

it('should log Locked event', async function () {
await expect(vault.connect(bridge).lock(mockToken.address, deployerAddress, 999))
.to.emit(vault, 'Locked')
.withArgs(await bridge.getAddress(), mockToken.address, deployerAddress, 999);
});
});

describe('TokenVault:unlock:accessControl', async () => {
beforeEach('setup TokenVault contract', async function () {
await setupContracts();
await vault.connect(deployer).addBridgeGateway(bridgeAddress);
await vault.connect(deployer).addBridgeGateway(otherBridgeAddress);
await mockToken.connect(deployer).approve(vault.address, 10000);
await vault.connect(bridge).lock(mockToken.address, deployerAddress, 9999);
});

it('should NOT be callable by non-bridge', async function () {
await expect(
vault.connect(deployer).unlock(mockToken.address, deployerAddress, 123),
).to.be.revertedWith('TokenVault: Bridge gateway not whitelisted');
});

it('should be callable by bridge', async function () {
await expect(vault.connect(bridge).unlock(mockToken.address, deployerAddress, 9990)).to.not.be
.reverted;
await expect(vault.connect(otherBridge).unlock(mockToken.address, deployerAddress, 9)).to.not
.be.reverted;
});
});

describe('TokenVault:unlock', async () => {
beforeEach('setup TokenVault contract', async function () {
await setupContracts();
await vault.connect(deployer).addBridgeGateway(bridgeAddress);
await mockToken.connect(deployer).approve(vault.address, 10000);
await vault.connect(bridge).lock(mockToken.address, deployerAddress, 9999);
});

it('should transfer tokens from the contract to recipient', async function () {
const _b = await mockToken.balanceOf(deployerAddress);
await expect(vault.connect(bridge).unlock(mockToken.address, deployerAddress, 999))
.to.emit(mockToken, 'Transfer')
.withArgs(vault.address, deployerAddress, 999);
const b = await mockToken.balanceOf(deployerAddress);
expect(b.sub(_b)).to.eq(999);
});

it('should update total unlocked', async function () {
await vault.connect(bridge).unlock(mockToken.address, deployerAddress, 999);
expect(await vault.totalLocked(mockToken.address)).to.eq(9000);
});

it('should log Unlocked event', async function () {
await expect(vault.connect(bridge).unlock(mockToken.address, deployerAddress, 999))
.to.emit(vault, 'Unlocked')
.withArgs(await bridge.getAddress(), mockToken.address, deployerAddress, 999);
});
});

0 comments on commit 0935b0a

Please sign in to comment.