-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c121295
commit 0935b0a
Showing
2 changed files
with
291 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |