Skip to content

Instantly share code, notes, and snippets.

@souradeep-das
Last active July 1, 2020 23:34
Show Gist options
  • Save souradeep-das/1855d4dd88a5508d8fadc074e5c65a1b to your computer and use it in GitHub Desktop.
Save souradeep-das/1855d4dd88a5508d8fadc074e5c65a1b to your computer and use it in GitHub Desktop.
Atomic Swap for Fast Exits
pragma solidity 0.5.11;
import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol";
import "openzeppelin-solidity/contracts/token/ERC721/IERC721.sol";
contract AtomicSwap {
using SafeERC20 for IERC20;
struct Swap {
address initiator;
address recipient;
address token;
uint256 value;
uint256 initTimestamp;
uint256 timeLock;
string secret;
bytes32 hashLock;
bool isNft;
}
// swapId to Swap
// swapId = keccak256(initiator, recipient, hashLock)
mapping (bytes32 => Swap) public swapData;
/**
* @notice Make sure to approve the token for the contract first
* @dev Alice calls this to initiate a swap with ERC721
* @param recipient Bob in this case
* @param token ERC721 token address
* @param tokenId token to transfer
* @param hashLock the keccak256(secret), Alice needs to provide
* @param timeLock timeout in seconds
*/
function setupNFTSwap (address recipient, address token, uint256 tokenId, bytes32 hashLock, uint256 timeLock) public {
bytes32 swapId = getSwapId(msg.sender, recipient, hashLock);
require(swapData[swapId].initiator == address(0), "hashLock already used");
IERC721(token).safeTransferFrom(msg.sender, address(this), tokenId);
swapData[swapId] = Swap(msg.sender, recipient, token, tokenId, block.timestamp, timeLock, "", hashLock, true);
}
/**
* @dev Bob calls this to participate in a swap with ETH
* @param recipient Alice in this case
* @param hashLock the keccak256(secret), same with Alice's hashLock
* @param timeLock timeout in seconds, Bob's timelock < Alice's timelock
*/
function setupEthSwap (address recipient, bytes32 hashLock, uint256 timeLock) public payable {
bytes32 swapId = getSwapId(msg.sender, recipient, hashLock);
require(swapData[swapId].initiator == address(0), "hashLock already used");
address(this).transfer(msg.value);
swapData[swapId] = Swap(msg.sender, recipient, address(0), msg.value, block.timestamp, timeLock, "", hashLock, false);
}
/**
* @notice Make sure to approve the token for the contract first
* @dev Bob calls this to participate in a swap with ERC20
* @param recipient Alice in this case
* @param token ERC20 token address
* @param value number of tokens
* @param hashLock the keccak256(secret), same with Alice's hashLock
* @param timeLock timeout in seconds, Bob's timelock < Alice's timelock
*/
function setupErc20Swap (address recipient, address token, uint256 value, bytes32 hashLock, uint256 timeLock) public {
bytes32 swapId = getSwapId(msg.sender, recipient, hashLock);
require(swapData[swapId].initiator == address(0), "hashLock already used");
IERC20(token).safeTransferFrom(msg.sender, address(this), value);
swapData[swapId] = Swap(msg.sender, recipient, token, value, block.timestamp, timeLock, "", hashLock, false);
}
/**
* @dev Alice and Bob both can use this to redeem the funds
* @param secret Alice's secret which produces hashLock
* @param swapId other party's swap identifier, eg: when Alice calls, swapId = keccak256(Bob, Alice, hashLock)
*/
function redeem(string memory secret, bytes32 swapId) public {
require(swapData[swapId].recipient == msg.sender, "Only Recipient specified can redeem");
require(swapData[swapId].hashLock == keccak256(bytes(secret)), "Incorrect secret provided");
require(block.timestamp < swapData[swapId].initTimestamp + swapData[swapId].timeLock, "Timeout");
if (swapData[swapId].token == address(0)) {
msg.sender.transfer(swapData[swapId].value);
}
else {
if(swapData[swapId].isNft == true) {
IERC721(swapData[swapId].token).safeTransferFrom(address(this), msg.sender, swapData[swapId].value);
}
else {
IERC20(swapData[swapId].token).safeTransfer(msg.sender, swapData[swapId].value);
}
}
swapData[swapId].recipient = address(0);
swapData[swapId].secret = secret;
}
/**
* @dev Alice and Bob both can use this get back unclaimed funds
* @param swapId own swap identifier, eg: when Alice calls, swapId = keccak256(Alice, Bob, hashLock)
*/
function refund(bytes32 swapId) public {
require(swapData[swapId].initiator == msg.sender, "Only Initiator can claim funds back after period");
require(block.timestamp > swapData[swapId].initTimestamp + swapData[swapId].timeLock, "Still under time lock");
require(swapData[swapId].recipient != address(0), "The swap was redeemed");
if (swapData[swapId].token == address(0)) {
msg.sender.transfer(swapData[swapId].value);
}
else {
if(swapData[swapId].isNft == true) {
IERC721(swapData[swapId].token).safeTransferFrom(address(this), msg.sender, swapData[swapId].value);
}
else {
IERC20(swapData[swapId].token).safeTransfer(msg.sender, swapData[swapId].value);
}
}
swapData[swapId].initiator = address(0);
}
/**
* @dev Can call this to get the swapId of any swap
* @param initiator Funds Provider
* @param recipient Funds Redeemer
* @param hashLock common keccak256(secret)
*/
function getSwapId(address initiator, address recipient, bytes32 hashLock) public view returns(bytes32) {
return keccak256(abi.encodePacked(initiator, recipient, hashLock));
}
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns(bytes4) {
return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
}
function () external payable {
}
}
const PaymentExitGame = artifacts.require('PaymentExitGame');
const PlasmaFramework = artifacts.require('PlasmaFramework');
const Liquidity = artifacts.require('../Liquidity');
const AtomicSwap = artifacts.require('../AtomicSwap');
const EthVault = artifacts.require('EthVault');
const Erc20Vault = artifacts.require('Erc20Vault');
const ERC20Mintable = artifacts.require('ERC20Mintable');
const {
BN, constants, expectRevert, time,
} = require('openzeppelin-test-helpers');
const { expect } = require('chai');
const { MerkleTree } = require('../helpers/merkle.js');
const { PaymentTransactionOutput, PaymentTransaction } = require('../helpers/transaction.js');
const { computeNormalOutputId, spentOnGas } = require('../helpers/utils.js');
const { buildUtxoPos } = require('../helpers/positions.js');
const Testlang = require('../helpers/testlang.js');
const config = require('../../config.js');
const { keccak256 } = require('ethereumjs-util');
contract(
'LiquidityContract - Fast Exits - End to End Tests',
([_deployer, maintainer, authority, bob, richDad]) => {
const ETH = constants.ZERO_ADDRESS;
const OUTPUT_TYPE_PAYMENT = config.registerKeys.outputTypes.payment;
const INITIAL_ERC20_SUPPLY = 10000000000;
const DEPOSIT_VALUE = 1000000;
const MERKLE_TREE_DEPTH = 16;
const TRANSFER_AMOUNT = 1000;
const alicePrivateKey = '0x7151e5dab6f8e95b5436515b83f423c4df64fe4c6149f864daa209b26adb10ca';
let alice;
const setupAccount = async () => {
const password = 'password1234';
alice = await web3.eth.personal.importRawKey(alicePrivateKey, password);
alice = web3.utils.toChecksumAddress(alice);
web3.eth.personal.unlockAccount(alice, password, 3600);
web3.eth.sendTransaction({ to: alice, from: richDad, value: web3.utils.toWei('1', 'ether') });
};
const deployStableContracts = async () => {
this.erc20 = await ERC20Mintable.new();
await this.erc20.mint(richDad, INITIAL_ERC20_SUPPLY);
};
before(async () => {
await Promise.all([setupAccount(), deployStableContracts()]);
});
const setupContracts = async () => {
this.framework = await PlasmaFramework.deployed();
this.ethVault = await EthVault.at(await this.framework.vaults(config.registerKeys.vaultId.eth));
this.erc20Vault = await Erc20Vault.at(await this.framework.vaults(config.registerKeys.vaultId.erc20));
this.exitGame = await PaymentExitGame.at(
await this.framework.exitGames(config.registerKeys.txTypes.payment),
);
this.startStandardExitBondSize = await this.exitGame.startStandardExitBondSize();
this.framework.addExitQueue(config.registerKeys.vaultId.eth, ETH);
this.liquidity = await Liquidity.new(this.framework.address, { from: authority });
};
const aliceDepositsETH = async () => {
const depositBlockNum = (await this.framework.nextDepositBlock()).toNumber();
this.depositUtxoPos = buildUtxoPos(depositBlockNum, 0, 0);
this.depositTx = Testlang.deposit(OUTPUT_TYPE_PAYMENT, DEPOSIT_VALUE, alice);
this.merkleTreeForDepositTx = new MerkleTree([this.depositTx], MERKLE_TREE_DEPTH);
this.merkleProofForDepositTx = this.merkleTreeForDepositTx.getInclusionProof(this.depositTx);
return this.ethVault.deposit(this.depositTx, { from: alice, value: DEPOSIT_VALUE });
};
const aliceTransferSomeEthToLC = async () => {
const tranferTxBlockNum = (await this.framework.nextChildBlock()).toNumber();
this.transferUtxoPos = buildUtxoPos(tranferTxBlockNum, 0, 0);
const outputLC = new PaymentTransactionOutput(
OUTPUT_TYPE_PAYMENT,
TRANSFER_AMOUNT,
this.liquidity.address,
ETH,
);
const outputAlice = new PaymentTransactionOutput(
OUTPUT_TYPE_PAYMENT,
DEPOSIT_VALUE - TRANSFER_AMOUNT,
alice,
ETH,
);
this.transferTxObject = new PaymentTransaction(1, [this.depositUtxoPos], [outputLC, outputAlice]);
this.transferTx = web3.utils.bytesToHex(this.transferTxObject.rlpEncoded());
this.merkleTreeForTransferTx = new MerkleTree([this.transferTx]);
this.merkleProofForTransferTx = this.merkleTreeForTransferTx.getInclusionProof(this.transferTx);
await this.framework.submitBlock(this.merkleTreeForTransferTx.root, { from: authority });
};
const bobDepositsETH = async () => {
const depositBlockNum = (await this.framework.nextDepositBlock()).toNumber();
this.bobDepositUtxoPos = buildUtxoPos(depositBlockNum, 0, 0);
this.bobDepositTx = Testlang.deposit(OUTPUT_TYPE_PAYMENT, DEPOSIT_VALUE, bob);
this.bobMerkleTreeForDepositTx = new MerkleTree([this.bobDepositTx], MERKLE_TREE_DEPTH);
this.bobMerkleProofForDepositTx = this.bobMerkleTreeForDepositTx.getInclusionProof(this.bobDepositTx);
return this.ethVault.deposit(this.bobDepositTx, { from: bob, value: DEPOSIT_VALUE });
};
const bobTransferSomeEthToLC = async () => {
const tranferTxBlockNum = (await this.framework.nextChildBlock()).toNumber();
this.bobTransferUtxoPos = buildUtxoPos(tranferTxBlockNum, 0, 0);
const outputLC = new PaymentTransactionOutput(
OUTPUT_TYPE_PAYMENT,
TRANSFER_AMOUNT,
this.liquidity.address,
ETH,
);
const outputBob = new PaymentTransactionOutput(
OUTPUT_TYPE_PAYMENT,
DEPOSIT_VALUE - TRANSFER_AMOUNT,
bob,
ETH,
);
this.bobTransferTxObject = new PaymentTransaction(1, [this.bobDepositUtxoPos], [outputLC, outputBob]);
this.bobTransferTx = web3.utils.bytesToHex(this.bobTransferTxObject.rlpEncoded());
this.bobMerkleTreeForTransferTx = new MerkleTree([this.bobTransferTx]);
this.bobMerkleProofForTransferTx = this.bobMerkleTreeForTransferTx.getInclusionProof(this.bobTransferTx);
await this.framework.submitBlock(this.bobMerkleTreeForTransferTx.root, { from: authority });
};
const aliceDepositsErc20 = async () => {
const depositBlockNum = (await this.framework.nextDepositBlock()).toNumber();
this.depositUtxoPos = buildUtxoPos(depositBlockNum, 0, 0);
this.depositTx = Testlang.deposit(OUTPUT_TYPE_PAYMENT, DEPOSIT_VALUE, alice, this.erc20.address);
this.merkleTreeForDepositTx = new MerkleTree([this.depositTx], MERKLE_TREE_DEPTH);
this.merkleProofForDepositTx = this.merkleTreeForDepositTx.getInclusionProof(this.depositTx);
return this.erc20Vault.deposit(this.depositTx, { from: alice });
};
const aliceTransferSomeErcToLC = async () => {
const tranferTxBlockNum = (await this.framework.nextChildBlock()).toNumber();
this.transferUtxoPos = buildUtxoPos(tranferTxBlockNum, 0, 0);
const outputLC = new PaymentTransactionOutput(
OUTPUT_TYPE_PAYMENT,
TRANSFER_AMOUNT,
this.liquidity.address,
this.erc20.address,
);
const outputAlice = new PaymentTransactionOutput(
OUTPUT_TYPE_PAYMENT,
DEPOSIT_VALUE - TRANSFER_AMOUNT,
alice,
this.erc20.address,
);
this.transferTxObject = new PaymentTransaction(1, [this.depositUtxoPos], [outputLC, outputAlice]);
this.transferTx = web3.utils.bytesToHex(this.transferTxObject.rlpEncoded());
this.merkleTreeForTransferTx = new MerkleTree([this.transferTx]);
this.merkleProofForTransferTx = this.merkleTreeForTransferTx.getInclusionProof(this.transferTx);
await this.framework.submitBlock(this.merkleTreeForTransferTx.root, { from: authority });
};
describe('Given contracts deployed, exit game and ETH vault registered', () => {
before(setupContracts);
describe('Given Alice deposited ETH and transferred some value to the Liquidity Contract', () => {
before(async () => {
await aliceDepositsETH();
await aliceTransferSomeEthToLC();
});
describe('And Alice starts the exit through LC and receives the NFT', () => {
before(async () => {
const utxoPos = this.transferUtxoPos;
const rlpOutputTx = this.transferTx;
const outputTxInclusionProof = this.merkleProofForTransferTx;
const { depositUtxoPos } = this;
const rlpDepositTx = this.depositTx;
const depositInclusionProof = this.merkleProofForDepositTx;
await this.liquidity.startExit(
utxoPos,
rlpOutputTx,
outputTxInclusionProof,
rlpDepositTx,
depositInclusionProof,
depositUtxoPos,
{ from: alice, value: this.startStandardExitBondSize },
);
});
describe('When Alice initiates an atomic swap to trade the NFT to Bob', () => {
before(async () => {
this.exitId = await this.exitGame.getStandardExitId(
false,
this.transferTx,
this.transferUtxoPos,
);
this.atomicSwap = await AtomicSwap.new({ from: authority });
await this.liquidity.approve(this.atomicSwap.address, this.exitId, { from: alice});
// secret = "abcd", keccak256("abcd") = 0x48bed44d1bcd124a28c27f343a817e5f5243190d3c52bf347daf876de1dbbf77
await this.atomicSwap.setupNFTSwap(bob, this.liquidity.address, this.exitId, "0x48bed44d1bcd124a28c27f343a817e5f5243190d3c52bf347daf876de1dbbf77", 5000, { from: alice});
this.aliceSwapId = await this.atomicSwap.getSwapId(alice, bob, "0x48bed44d1bcd124a28c27f343a817e5f5243190d3c52bf347daf876de1dbbf77");
});
it('should create a new swap', async () => {
const aliceSwap = await this.atomicSwap.swapData(this.aliceSwapId);
expect(aliceSwap.initiator).to.be.equal(alice);
expect(aliceSwap.recipient).to.be.equal(bob);
expect(aliceSwap.value).to.be.bignumber.equal(this.exitId);
const nftOwner = await this.liquidity.ownerOf(this.exitId);
expect(nftOwner).to.be.equal(this.atomicSwap.address);
});
describe('When Bob participates to Alice\'s swap with Eth in return', () => {
before(async () => {
this.swapValue = new BN(500000);
this.contractBalanceBeforeInitSwap = new BN(await web3.eth.getBalance(this.atomicSwap.address));
// uses the same hashLock provided by Alice
await this.atomicSwap.setupEthSwap(alice, "0x48bed44d1bcd124a28c27f343a817e5f5243190d3c52bf347daf876de1dbbf77", 2500, { from: bob, value: this.swapValue });
this.bobSwapId = await this.atomicSwap.getSwapId(bob, alice, "0x48bed44d1bcd124a28c27f343a817e5f5243190d3c52bf347daf876de1dbbf77");
});
it('should successfully participate', async () => {
const bobSwap = await this.atomicSwap.swapData(this.bobSwapId);
expect(bobSwap.initiator).to.be.equal(bob);
expect(bobSwap.recipient).to.be.equal(alice);
expect(bobSwap.value).to.be.bignumber.equal(this.swapValue);
const expectedBalanceAfterInitSwap = this.contractBalanceBeforeInitSwap
.add(this.swapValue);
const actualBalanceAfterInitSwap = new BN(await web3.eth.getBalance(this.atomicSwap.address));
expect(expectedBalanceAfterInitSwap).to.be.bignumber.equal(actualBalanceAfterInitSwap)
});
describe('When Alice redeems Bob\'s proposal', () => {
before(async () => {
this.aliceBalanceBeforeSwap = new BN(await web3.eth.getBalance(alice));
// redeem Bob's proposal hence bob's swapId
const { receipt } = await this.atomicSwap.redeem("abcd", this.bobSwapId, { from: alice });
this.aliceReceipt = receipt;
});
it('should send the ether to Alice', async () => {
const expectedAliceBalanceAfterSwap = this.aliceBalanceBeforeSwap
.add(this.swapValue)
.sub(await spentOnGas(this.aliceReceipt));
const actualAliceBalanceAfterSwap = new BN(await web3.eth.getBalance(alice));
expect(expectedAliceBalanceAfterSwap).to.be.bignumber.equal(actualAliceBalanceAfterSwap);
});
it('should save the secret for Bob to redeem', async () => {
const bobSwap = await this.atomicSwap.swapData(this.bobSwapId);
expect(bobSwap.secret).to.be.equal("abcd");
});
describe('When Bob redeems Alice\'s proposal with the secret', () => {
before(async () => {
// redeem Alice's proposal with secret
const bobSwap = await this.atomicSwap.swapData(this.bobSwapId);
await this.atomicSwap.redeem(bobSwap.secret, this.aliceSwapId, { from: bob });
});
it('should send the token to Bob', async () => {
const nftOwner = await this.liquidity.ownerOf(this.exitId);
expect(nftOwner).to.be.equal(bob);
});
});
});
});
describe('And then someone processes the exits after two weeks', () => {
before(async () => {
await time.increase(time.duration.weeks(2).add(time.duration.seconds(1)));
await this.framework.processExits(config.registerKeys.vaultId.eth, ETH, 0, 1);
});
describe('When Alice tries to claim funds back', () => {
it('should not be successful', async () => {
await expectRevert(
this.liquidity.withdrawExit(this.exitId, { from: alice }),
'Only the NFT owner of the respective exit can withdraw',
);
});
});
describe('When Bob tries to get exit bond', () => {
it('should not be successful', async () => {
await expectRevert(
this.liquidity.withdrawExitBond(this.exitId, { from: bob }),
'Only the Exit Initiator can claim the bond',
);
});
});
describe('When Alice tries to get exit bond back', () => {
before(async () => {
this.aliceBalanceBeforeClaiming = new BN(await web3.eth.getBalance(alice));
const { receipt } = await this.liquidity.withdrawExitBond(this.exitId, {
from: alice,
});
this.aliceWithdrawalReceipt = receipt;
});
it('should return the exit bond to Alice', async () => {
const actualAliceBalanceAfterClaiming = new BN(await web3.eth.getBalance(alice));
const expectedAliceBalance = this.aliceBalanceBeforeClaiming.add(
this.startStandardExitBondSize,
).sub(await spentOnGas(this.aliceWithdrawalReceipt));
expect(actualAliceBalanceAfterClaiming).to.be.bignumber.equal(
expectedAliceBalance,
);
});
});
describe('When Bob tries to claim funds back through the NFT', () => {
before(async () => {
this.bobBalanceBeforeClaiming = new BN(await web3.eth.getBalance(bob));
const { receipt } = await this.liquidity.withdrawExit(this.exitId, {
from: bob,
});
this.bobWithdrawalReceipt = receipt;
});
it('should return the amount to Bob', async () => {
const actualBobBalanceAfterWithdrawal = new BN(await web3.eth.getBalance(bob));
const expectedBobBalance = this.bobBalanceBeforeClaiming
.add(new BN(this.transferTxObject.outputs[0].amount))
.sub(await spentOnGas(this.bobWithdrawalReceipt));
expect(actualBobBalanceAfterWithdrawal).to.be.bignumber.equal(expectedBobBalance);
});
it('should burn the NFT', async () => {
await expectRevert(
this.liquidity.ownerOf(this.exitId),
'ERC721: owner query for nonexistent token',
);
});
});
});
});
});
});
});
},
);
@souradeep-das
Copy link
Author

souradeep-das commented Jul 1, 2020

See L200-L276 where i have replaced the test's to use atomic swap for swapping NFT with ETH (covering the workflow to be used in the demo)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment