456 lines
16 KiB
Solidity
456 lines
16 KiB
Solidity
|
// SPDX-License-Identifier: CC0-1.0
|
||
|
// Copyright (c) 2019 Chris Haoyu LIN, Runchao HAN, Jiangshan YU
|
||
|
// ERC2266 is compatible with ERC20 standard: https://theethereum.wiki/w/index.php/ERC20_Token_Standard
|
||
|
// naming style follows the guide: https://solidity.readthedocs.io/en/v0.5.11/style-guide.html#naming-styles
|
||
|
|
||
|
pragma solidity ^0.5.11;
|
||
|
|
||
|
contract ERC20 {
|
||
|
function totalSupply() public view returns (uint);
|
||
|
function balanceOf(address tokenOwner) public view returns (uint balance);
|
||
|
function allowance(address tokenOwner, address spender) public view returns (uint remaining);
|
||
|
function transfer(address to, uint tokens) public returns (bool success);
|
||
|
function approve(address spender, uint tokens) public returns (bool success);
|
||
|
function transferFrom(address from, address to, uint tokens) public returns (bool success);
|
||
|
event Transfer(address indexed from, address indexed to, uint tokens);
|
||
|
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
|
||
|
}
|
||
|
|
||
|
contract Example
|
||
|
{
|
||
|
enum AssetState { Empty, Filled, Redeemed, Refunded }
|
||
|
|
||
|
struct Swap {
|
||
|
bytes32 secretHash;
|
||
|
bytes32 secret;
|
||
|
address payable initiator;
|
||
|
address payable participant;
|
||
|
address tokenA;
|
||
|
address tokenB;
|
||
|
}
|
||
|
|
||
|
struct InitiatorAsset {
|
||
|
uint256 amount;
|
||
|
uint256 refundTimestamp;
|
||
|
AssetState state;
|
||
|
}
|
||
|
|
||
|
struct ParticipantAsset {
|
||
|
uint256 amount;
|
||
|
uint256 refundTimestamp;
|
||
|
AssetState state;
|
||
|
}
|
||
|
|
||
|
struct Premium {
|
||
|
uint256 amount;
|
||
|
uint256 refundTimestamp;
|
||
|
AssetState state;
|
||
|
}
|
||
|
|
||
|
mapping(bytes32 => Swap) public swap;
|
||
|
mapping(bytes32 => InitiatorAsset) public initiatorAsset;
|
||
|
mapping(bytes32 => ParticipantAsset) public participantAsset;
|
||
|
mapping(bytes32 => Premium) public premium;
|
||
|
|
||
|
event SetUp(
|
||
|
bytes32 secretHash,
|
||
|
address initiator,
|
||
|
address participant,
|
||
|
address tokenA,
|
||
|
address tokenB,
|
||
|
uint256 initiatorAssetAmount,
|
||
|
uint256 participantAssetAmount,
|
||
|
uint256 premiumAmount
|
||
|
);
|
||
|
|
||
|
event Initiated(
|
||
|
uint256 initiateTimestamp,
|
||
|
bytes32 secretHash,
|
||
|
address initiator,
|
||
|
address participant,
|
||
|
address initiatorAssetToken,
|
||
|
uint256 initiatorAssetAmount,
|
||
|
uint256 initiatorAssetRefundTimestamp
|
||
|
);
|
||
|
|
||
|
event PremiumFilled(
|
||
|
uint256 fillPremiumTimestamp,
|
||
|
bytes32 secretHash,
|
||
|
address initiator,
|
||
|
address participant,
|
||
|
address premiumToken,
|
||
|
uint256 premiumAmount,
|
||
|
uint256 premiumRefundTimestamp
|
||
|
);
|
||
|
|
||
|
event Participated(
|
||
|
uint256 participateTimestamp,
|
||
|
bytes32 secretHash,
|
||
|
address initiator,
|
||
|
address participant,
|
||
|
address participantAssetToken,
|
||
|
uint256 participantAssetAmount,
|
||
|
uint256 participantAssetRefundTimestamp
|
||
|
);
|
||
|
|
||
|
event InitiatorAssetRedeemed(
|
||
|
uint256 redeemTimestamp,
|
||
|
bytes32 secretHash,
|
||
|
bytes32 secret,
|
||
|
address redeemer,
|
||
|
address assetToken,
|
||
|
uint256 amount
|
||
|
);
|
||
|
|
||
|
event ParticipantAssetRedeemed(
|
||
|
uint256 redeemTimestamp,
|
||
|
bytes32 secretHash,
|
||
|
bytes32 secret,
|
||
|
address redeemer,
|
||
|
address assetToken,
|
||
|
uint256 amount
|
||
|
);
|
||
|
|
||
|
event InitiatorAssetRefunded(
|
||
|
uint256 refundTimestamp,
|
||
|
bytes32 secretHash,
|
||
|
address refunder,
|
||
|
address assetToken,
|
||
|
uint256 amount
|
||
|
);
|
||
|
|
||
|
event ParticipantAssetRefunded(
|
||
|
uint256 refundTimestamp,
|
||
|
bytes32 secretHash,
|
||
|
address refunder,
|
||
|
address assetToken,
|
||
|
uint256 amount
|
||
|
);
|
||
|
|
||
|
event PremiumRedeemed(
|
||
|
uint256 redeemTimestamp,
|
||
|
bytes32 secretHash,
|
||
|
address redeemer,
|
||
|
address token,
|
||
|
uint256 amount
|
||
|
);
|
||
|
|
||
|
event PremiumRefunded(
|
||
|
uint256 refundTimestamp,
|
||
|
bytes32 secretHash,
|
||
|
address refunder,
|
||
|
address token,
|
||
|
uint256 amount
|
||
|
);
|
||
|
|
||
|
constructor() public {}
|
||
|
|
||
|
modifier isInitiatorAssetEmptyState(bytes32 secretHash) {
|
||
|
require(initiatorAsset[secretHash].state == AssetState.Empty);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier isParticipantAssetEmptyState(bytes32 secretHash) {
|
||
|
require(participantAsset[secretHash].state == AssetState.Empty);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier isPremiumEmptyState(bytes32 secretHash) {
|
||
|
require(premium[secretHash].state == AssetState.Empty);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier canSetup(bytes32 secretHash) {
|
||
|
require(initiatorAsset[secretHash].state == AssetState.Empty);
|
||
|
require(participantAsset[secretHash].state == AssetState.Empty);
|
||
|
require(premium[secretHash].state == AssetState.Empty);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier canInitiate(bytes32 secretHash) {
|
||
|
require(swap[secretHash].initiator == msg.sender);
|
||
|
require(initiatorAsset[secretHash].state == AssetState.Empty);
|
||
|
require(ERC20(swap[secretHash].tokenA).balanceOf(msg.sender) >= initiatorAsset[secretHash].amount);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier canFillPremium(bytes32 secretHash) {
|
||
|
require(swap[secretHash].initiator == msg.sender);
|
||
|
require(premium[secretHash].state == AssetState.Empty);
|
||
|
require(ERC20(swap[secretHash].tokenB).balanceOf(msg.sender) >= premium[secretHash].amount);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier canParticipate(bytes32 secretHash) {
|
||
|
require(swap[secretHash].participant == msg.sender);
|
||
|
require(participantAsset[secretHash].state == AssetState.Empty);
|
||
|
require(premium[secretHash].state == AssetState.Filled);
|
||
|
require(ERC20(swap[secretHash].tokenB).balanceOf(msg.sender) >= participantAsset[secretHash].amount);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier checkRefundTimestampOverflow(uint256 refundTime) {
|
||
|
uint256 refundTimestamp = block.timestamp + refundTime;
|
||
|
require(refundTimestamp > block.timestamp, "calc refundTimestamp overflow");
|
||
|
require(refundTimestamp > refundTime, "calc refundTimestamp overflow");
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier isAssetRedeemable(bytes32 secretHash, bytes32 secret) {
|
||
|
if (swap[secretHash].initiator == msg.sender) {
|
||
|
require(initiatorAsset[secretHash].state == AssetState.Filled);
|
||
|
require(block.timestamp <= initiatorAsset[secretHash].refundTimestamp);
|
||
|
} else {
|
||
|
require(swap[secretHash].participant == msg.sender);
|
||
|
require(participantAsset[secretHash].state == AssetState.Filled);
|
||
|
require(block.timestamp <= participantAsset[secretHash].refundTimestamp);
|
||
|
}
|
||
|
require(sha256(abi.encodePacked(secret)) == secretHash);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier isAssetRefundable(bytes32 secretHash) {
|
||
|
if (swap[secretHash].initiator == msg.sender) {
|
||
|
require(initiatorAsset[secretHash].state == AssetState.Filled);
|
||
|
require(block.timestamp > initiatorAsset[secretHash].refundTimestamp);
|
||
|
} else {
|
||
|
require(swap[secretHash].participant == msg.sender);
|
||
|
require(participantAsset[secretHash].state == AssetState.Filled);
|
||
|
require(block.timestamp > participantAsset[secretHash].refundTimestamp);
|
||
|
}
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
modifier isPremiumFilledState(bytes32 secretHash) {
|
||
|
require(premium[secretHash].state == AssetState.Filled);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
// Premium is redeemable for Bob if Bob participates and redeem
|
||
|
// before premium's timelock expires
|
||
|
modifier isPremiumRedeemable(bytes32 secretHash) {
|
||
|
// the participant invokes this method to redeem the premium
|
||
|
require(swap[secretHash].participant == msg.sender);
|
||
|
// the premium should be deposited
|
||
|
require(premium[secretHash].state == AssetState.Filled);
|
||
|
// if Bob participates, which means participantAsset will be: Filled -> (Redeemed/Refunded)
|
||
|
require(participantAsset[secretHash].state == AssetState.Refunded || participantAsset[secretHash].state == AssetState.Redeemed);
|
||
|
// the premium timelock should not be expired
|
||
|
require(block.timestamp <= premium[secretHash].refundTimestamp);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
// Premium is refundable for Alice only when Alice initiates
|
||
|
// but Bob does not participate after premium's timelock expires
|
||
|
modifier isPremiumRefundable(bytes32 secretHash) {
|
||
|
// the initiator invokes this method to refund the premium
|
||
|
require(swap[secretHash].initiator == msg.sender);
|
||
|
// the premium should be deposited
|
||
|
require(premium[secretHash].state == AssetState.Filled);
|
||
|
// asset2 should be empty
|
||
|
// which means Bob does not participate
|
||
|
require(participantAsset[secretHash].state == AssetState.Empty);
|
||
|
require(block.timestamp > premium[secretHash].refundTimestamp);
|
||
|
_;
|
||
|
}
|
||
|
|
||
|
function setup(bytes32 secretHash,
|
||
|
address payable initiator,
|
||
|
address tokenA,
|
||
|
address tokenB,
|
||
|
uint256 initiatorAssetAmount,
|
||
|
address payable participant,
|
||
|
uint256 participantAssetAmount,
|
||
|
uint256 premiumAmount)
|
||
|
public
|
||
|
payable
|
||
|
canSetup(secretHash)
|
||
|
{
|
||
|
swap[secretHash].secretHash = secretHash;
|
||
|
swap[secretHash].initiator = initiator;
|
||
|
swap[secretHash].participant = participant;
|
||
|
swap[secretHash].tokenA = tokenA;
|
||
|
swap[secretHash].tokenB = tokenB;
|
||
|
initiatorAsset[secretHash].amount = initiatorAssetAmount;
|
||
|
initiatorAsset[secretHash].state = AssetState.Empty;
|
||
|
participantAsset[secretHash].amount = participantAssetAmount;
|
||
|
participantAsset[secretHash].state = AssetState.Empty;
|
||
|
premium[secretHash].amount = premiumAmount;
|
||
|
premium[secretHash].state = AssetState.Empty;
|
||
|
|
||
|
emit SetUp(
|
||
|
secretHash,
|
||
|
initiator,
|
||
|
participant,
|
||
|
tokenA,
|
||
|
tokenB,
|
||
|
initiatorAssetAmount,
|
||
|
participantAssetAmount,
|
||
|
premiumAmount
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Initiator needs to pay for the initiatorAsset(tokenA) with initiatorAssetAmount
|
||
|
// Initiator will also need to call tokenA.approve(this_contract_address, initiatorAssetAmount) in advance
|
||
|
function initiate(bytes32 secretHash, uint256 assetRefundTime)
|
||
|
public
|
||
|
payable
|
||
|
canInitiate(secretHash)
|
||
|
checkRefundTimestampOverflow(assetRefundTime)
|
||
|
{
|
||
|
ERC20(swap[secretHash].tokenA).transferFrom(swap[secretHash].initiator, address(this), initiatorAsset[secretHash].amount);
|
||
|
initiatorAsset[secretHash].state = AssetState.Filled;
|
||
|
initiatorAsset[secretHash].refundTimestamp = block.timestamp + assetRefundTime;
|
||
|
|
||
|
emit Initiated(
|
||
|
block.timestamp,
|
||
|
secretHash,
|
||
|
msg.sender,
|
||
|
swap[secretHash].participant,
|
||
|
swap[secretHash].tokenA,
|
||
|
initiatorAsset[secretHash].amount,
|
||
|
initiatorAsset[secretHash].refundTimestamp
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Initiator needs to pay for the premium(tokenB) with premiumAmount
|
||
|
// Initiator will also need to call tokenB.approve(this_contract_address, premiumAmount) in advance
|
||
|
function fillPremium(bytes32 secretHash, uint256 premiumRefundTime)
|
||
|
public
|
||
|
payable
|
||
|
canFillPremium(secretHash)
|
||
|
checkRefundTimestampOverflow(premiumRefundTime)
|
||
|
{
|
||
|
ERC20(swap[secretHash].tokenB).transferFrom(swap[secretHash].initiator, address(this), premium[secretHash].amount);
|
||
|
premium[secretHash].state = AssetState.Filled;
|
||
|
premium[secretHash].refundTimestamp = block.timestamp + premiumRefundTime;
|
||
|
|
||
|
emit PremiumFilled(
|
||
|
block.timestamp,
|
||
|
secretHash,
|
||
|
msg.sender,
|
||
|
swap[secretHash].participant,
|
||
|
swap[secretHash].tokenB,
|
||
|
premium[secretHash].amount,
|
||
|
premium[secretHash].refundTimestamp
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Participant needs to pay for the participantAsset(tokenB) with participantAssetAmount
|
||
|
// Participant will also need to call tokenB.approve(this_contract_address, participantAssetAmount) in advance
|
||
|
function participate(bytes32 secretHash, uint256 assetRefundTime)
|
||
|
public
|
||
|
payable
|
||
|
canParticipate(secretHash)
|
||
|
checkRefundTimestampOverflow(assetRefundTime)
|
||
|
{
|
||
|
ERC20(swap[secretHash].tokenB).transferFrom(swap[secretHash].participant, address(this), participantAsset[secretHash].amount);
|
||
|
participantAsset[secretHash].state = AssetState.Filled;
|
||
|
participantAsset[secretHash].refundTimestamp = block.timestamp + assetRefundTime;
|
||
|
|
||
|
emit Participated(
|
||
|
block.timestamp,
|
||
|
secretHash,
|
||
|
swap[secretHash].initiator,
|
||
|
msg.sender,
|
||
|
swap[secretHash].tokenB,
|
||
|
participantAsset[secretHash].amount,
|
||
|
participantAsset[secretHash].refundTimestamp
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function redeemAsset(bytes32 secret, bytes32 secretHash)
|
||
|
public
|
||
|
isAssetRedeemable(secretHash, secret)
|
||
|
{
|
||
|
swap[secretHash].secret = secret;
|
||
|
if (swap[secretHash].initiator == msg.sender) {
|
||
|
ERC20(swap[secretHash].tokenB).transfer(msg.sender, participantAsset[secretHash].amount);
|
||
|
participantAsset[secretHash].state = AssetState.Redeemed;
|
||
|
|
||
|
emit ParticipantAssetRedeemed(
|
||
|
block.timestamp,
|
||
|
secretHash,
|
||
|
secret,
|
||
|
msg.sender,
|
||
|
swap[secretHash].tokenB,
|
||
|
participantAsset[secretHash].amount
|
||
|
);
|
||
|
} else {
|
||
|
ERC20(swap[secretHash].tokenA).transfer(msg.sender, initiatorAsset[secretHash].amount);
|
||
|
initiatorAsset[secretHash].state = AssetState.Redeemed;
|
||
|
|
||
|
emit InitiatorAssetRedeemed(
|
||
|
block.timestamp,
|
||
|
secretHash,
|
||
|
secret,
|
||
|
msg.sender,
|
||
|
swap[secretHash].tokenA,
|
||
|
initiatorAsset[secretHash].amount
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function refundAsset(bytes32 secretHash)
|
||
|
public
|
||
|
isPremiumFilledState(secretHash)
|
||
|
isAssetRefundable(secretHash)
|
||
|
{
|
||
|
if (swap[secretHash].initiator == msg.sender) {
|
||
|
ERC20(swap[secretHash].tokenA).transfer(msg.sender, initiatorAsset[secretHash].amount);
|
||
|
initiatorAsset[secretHash].state = AssetState.Refunded;
|
||
|
|
||
|
emit InitiatorAssetRefunded(
|
||
|
block.timestamp,
|
||
|
secretHash,
|
||
|
msg.sender,
|
||
|
swap[secretHash].tokenA,
|
||
|
initiatorAsset[secretHash].amount
|
||
|
);
|
||
|
} else {
|
||
|
ERC20(swap[secretHash].tokenB).transfer(msg.sender, participantAsset[secretHash].amount);
|
||
|
participantAsset[secretHash].state = AssetState.Refunded;
|
||
|
|
||
|
emit ParticipantAssetRefunded(
|
||
|
block.timestamp,
|
||
|
secretHash,
|
||
|
msg.sender,
|
||
|
swap[secretHash].tokenB,
|
||
|
participantAsset[secretHash].amount
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function redeemPremium(bytes32 secretHash)
|
||
|
public
|
||
|
isPremiumRedeemable(secretHash)
|
||
|
{
|
||
|
ERC20(swap[secretHash].tokenB).transfer(msg.sender, premium[secretHash].amount);
|
||
|
premium[secretHash].state = AssetState.Redeemed;
|
||
|
|
||
|
emit PremiumRefunded(
|
||
|
block.timestamp,
|
||
|
swap[secretHash].secretHash,
|
||
|
msg.sender,
|
||
|
swap[secretHash].tokenB,
|
||
|
premium[secretHash].amount
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function refundPremium(bytes32 secretHash)
|
||
|
public
|
||
|
isPremiumRefundable(secretHash)
|
||
|
{
|
||
|
ERC20(swap[secretHash].tokenB).transfer(msg.sender, premium[secretHash].amount);
|
||
|
premium[secretHash].state = AssetState.Refunded;
|
||
|
|
||
|
emit PremiumRefunded(
|
||
|
block.timestamp,
|
||
|
swap[secretHash].secretHash,
|
||
|
msg.sender,
|
||
|
swap[secretHash].tokenB,
|
||
|
premium[secretHash].amount
|
||
|
);
|
||
|
}
|
||
|
}
|