Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a transfer function that charges a fee #17

Merged
merged 1 commit into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/contracts/contracts/BIP20/BIP20DelegatedTransfer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ contract BIP20DelegatedTransfer is BIP20, IBIP20DelegatedTransfer {
bytes calldata signature
) external override returns (bool) {
bytes32 dataHash = keccak256(abi.encode(block.chainid, address(this), from, to, amount, nonce[from], expiry));
require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), signature) == from, "Invalid signature");
require(expiry > block.timestamp, "Expired signature");
require(
ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), signature) == from,
"BIP20DelegatedTransfer: Invalid signature"
);
require(expiry > block.timestamp, "BIP20DelegatedTransfer: Expired signature");

super._transfer(from, to, amount);
nonce[from]++;
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/contracts/LYT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ contract LYT is LoyaltyToken {
/*
* Public functions
*/
constructor(address account_) LoyaltyToken("Loyalty Coin", "LYT", account_) {}
constructor(address account_, address feeAccount_) LoyaltyToken("Loyalty Coin", "LYT", account_, feeAccount_) {}
}
55 changes: 53 additions & 2 deletions packages/contracts/contracts/LoyaltyToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ contract LoyaltyToken is BIP20DelegatedTransfer {
*/
address internal owner;

address public protocolFeeAccount;

uint256 internal protocolFee;

uint256 public constant MAX_PROTOCOL_FEE = 5e18;

/*
* Modifiers
*/
Expand All @@ -24,11 +30,18 @@ contract LoyaltyToken is BIP20DelegatedTransfer {
/*
* Public functions
*/
constructor(string memory name_, string memory symbol_, address account_) BIP20DelegatedTransfer(name_, symbol_) {
constructor(
string memory name_,
string memory symbol_,
address account_,
address feeAccount_
) BIP20DelegatedTransfer(name_, symbol_) {
owner = account_;
protocolFeeAccount = feeAccount_;
protocolFee = 1e17;
require(
IMultiSigWallet(owner).supportsInterface(type(IMultiSigWallet).interfaceId),
"Invalid interface ID of multi sig wallet"
"LoyaltyToken: Invalid interface ID of multi sig wallet"
);
}

Expand All @@ -39,4 +52,42 @@ contract LoyaltyToken is BIP20DelegatedTransfer {
function getOwner() external view returns (address) {
return owner;
}

function getProtocolFee() external view returns (uint256) {
return protocolFee;
}

function changeProtocolFee(uint256 _protocolFee) external {
require(msg.sender == owner, "LoyaltyToken: Sender is not authorized to execute.");
require(_protocolFee <= MAX_PROTOCOL_FEE, "LoyaltyToken: The value entered is not an appropriate value.");
protocolFee = _protocolFee;
}

function changeProtocolFeeAccount(address _account) external {
require(msg.sender == protocolFeeAccount, "LoyaltyToken: Sender is not authorized to execute.");

protocolFeeAccount = _account;
}

function delegatedTransferWithFee(
address from,
address to,
uint256 amount,
uint256 expiry,
bytes calldata signature
) external returns (bool) {
bytes32 dataHash = keccak256(abi.encode(block.chainid, address(this), from, to, amount, nonce[from], expiry));
require(
ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), signature) == from,
"LoyaltyToken: Invalid signature"
);
require(expiry > block.timestamp, "LoyaltyToken: Expired signature");

require(amount > protocolFee, "LoyaltyToken: The amount should be greater than the fee.");
require(balanceOf(from) >= amount, "LoyaltyToken: transfer amount exceeds balance");
super._transfer(from, to, amount - protocolFee);
super._transfer(from, protocolFeeAccount, protocolFee);
nonce[from]++;
return true;
}
}
8 changes: 5 additions & 3 deletions packages/contracts/deploy/main_chain_devnet/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ interface IDeployedContract {

interface IAccount {
deployer: Wallet;
feeAccount: Wallet;
}

type FnDeployer = (accounts: IAccount, deployment: Deployments) => void;
type FnDeployer = (accounts: IAccount, deployment: Deployments) => Promise<any>;

class Deployments {
public deployments: Map<string, IDeployedContract>;
Expand All @@ -48,10 +49,11 @@ class Deployments {
this.deployers = [];

const raws = HardhatAccount.keys.map((m) => new Wallet(m, ethers.provider));
const [deployer] = raws;
const [deployer, feeAccount] = raws;

this.accounts = {
deployer,
feeAccount,
};
}

Expand Down Expand Up @@ -196,7 +198,7 @@ async function deployToken(accounts: IAccount, deployment: Deployments) {
const factory = await ethers.getContractFactory("LYT");
const contract = (await factory
.connect(accounts.deployer)
.deploy(deployment.getContractAddress("MultiSigWallet"))) as LYT;
.deploy(deployment.getContractAddress("MultiSigWallet"), deployment.accounts.feeAccount.address)) as LYT;
await contract.deployed();
await contract.deployTransaction.wait();

Expand Down
8 changes: 5 additions & 3 deletions packages/contracts/deploy/side_chain_devnet/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ interface IDeployedContract {

interface IAccount {
deployer: Wallet;
feeAccount: Wallet;
}

type FnDeployer = (accounts: IAccount, deployment: Deployments) => void;
type FnDeployer = (accounts: IAccount, deployment: Deployments) => Promise<any>;

class Deployments {
public deployments: Map<string, IDeployedContract>;
Expand All @@ -48,10 +49,11 @@ class Deployments {
this.deployers = [];

const raws = HardhatAccount.keys.map((m) => new Wallet(m, ethers.provider));
const [deployer] = raws;
const [deployer, feeAccount] = raws;

this.accounts = {
deployer,
feeAccount,
};
}

Expand Down Expand Up @@ -196,7 +198,7 @@ async function deployToken(accounts: IAccount, deployment: Deployments) {
const factory = await ethers.getContractFactory("LYT");
const contract = (await factory
.connect(accounts.deployer)
.deploy(deployment.getContractAddress("MultiSigWallet"))) as LYT;
.deploy(deployment.getContractAddress("MultiSigWallet"), deployment.accounts.feeAccount.address)) as LYT;
await contract.deployed();
await contract.deployTransaction.wait();

Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/env/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ TEST_NET_URL=https://testnet.bosagora.org
# 0x02eaFC1091533F984dB53483a7215c7a982a3Ac1
DEPLOYER=0xdf29fb48bf34751707572533b6d6b4544e9ca1efb4a1fce9442164b575c0c061

# 0x3633B7eBd5562316BD3740FAe1d5A4aD46DbD8f0
PROTOCOL_FEE=0xf077a67e3982b1fba2233cfec2f22680bf090f91e0b0f494ed557b31b5bf4b9f

REPORT_GAS=true
11 changes: 11 additions & 0 deletions packages/contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ function getAccounts() {
accounts.push(process.env.DEPLOYER);
}

if (
process.env.PROTOCOL_FEE !== undefined &&
process.env.PROTOCOL_FEE.trim() !== "" &&
reg_bytes64.test(process.env.PROTOCOL_FEE)
) {
accounts.push(process.env.PROTOCOL_FEE);
} else {
process.env.PROTOCOL_FEE = Wallet.createRandom().privateKey;
accounts.push(process.env.PROTOCOL_FEE);
}

while (accounts.length < 50) {
accounts.push(Wallet.createRandom().privateKey);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "loyalty-tokens",
"version": "2.0.0",
"version": "2.1.0",
"description": "Smart contracts for the loyalty tokens",
"files": [
"**/*.sol"
Expand Down
42 changes: 36 additions & 6 deletions packages/contracts/test/DelegatedTransfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ async function deployMultiSigWallet(
: undefined;
}

async function deployToken(deployer: Wallet, owner: string): Promise<LYT> {
async function deployToken(deployer: Wallet, owner: string, feeAccount: string): Promise<LYT> {
const factory = await ethers.getContractFactory("LYT");
const contract = (await factory.connect(deployer).deploy(owner)) as LYT;
const contract = (await factory.connect(deployer).deploy(owner, feeAccount)) as LYT;
await contract.deployed();
await contract.deployTransaction.wait();
return contract;
}

describe("Test for LYT token", () => {
const raws = HardhatAccount.keys.map((m) => new Wallet(m, ethers.provider));
const [deployer, account0, account1, account2, account3, account4, account5] = raws;
const [deployer, feeAccount, account0, account1, account2, account3, account4, account5] = raws;
const owners1 = [account0, account1, account2];

let multiSigFactory: MultiSigWalletFactory;
Expand Down Expand Up @@ -88,7 +88,7 @@ describe("Test for LYT token", () => {
it("Create Token, Owner is MultiSigWallet", async () => {
assert.ok(multiSigWallet);

token = await deployToken(deployer, multiSigWallet.address);
token = await deployToken(deployer, multiSigWallet.address, feeAccount.address);
assert.deepStrictEqual(await token.getOwner(), multiSigWallet.address);
assert.deepStrictEqual(await token.balanceOf(multiSigWallet.address), BigNumber.from(0));
});
Expand Down Expand Up @@ -173,7 +173,7 @@ describe("Test for LYT token", () => {
const signature = ContractUtils.signMessage(account3, message);
await expect(
token.delegatedTransfer(account4.address, account5.address, amount, expiry, signature)
).to.be.revertedWith("Invalid signature");
).to.be.revertedWith("BIP20DelegatedTransfer: Invalid signature");
});

it("Transfer from account4 to account5 - Expired signature", async () => {
Expand All @@ -192,7 +192,7 @@ describe("Test for LYT token", () => {
const signature = ContractUtils.signMessage(account4, message);
await expect(
token.delegatedTransfer(account4.address, account5.address, amount, expiry, signature)
).to.be.revertedWith("Expired signature");
).to.be.revertedWith("BIP20DelegatedTransfer: Expired signature");
});

it("Transfer from account4 to account5", async () => {
Expand All @@ -213,4 +213,34 @@ describe("Test for LYT token", () => {

assert.deepStrictEqual(await token.balanceOf(account5.address), amount);
});

it("Transfer with fee from account4 to account5", async () => {
const oldBalance4 = await token.balanceOf(account4.address);
const oldBalance5 = await token.balanceOf(account5.address);
const oldBalanceFee = await token.balanceOf(feeAccount.address);
const protocolFee = await token.getProtocolFee();

const amount = BOACoin.make(500).value;
const nonce = await token.nonceOf(account4.address);
const expiry = ContractUtils.getTimeStamp() + 12 * 5;
const message = ContractUtils.getTransferMessage(
ethers.provider.network.chainId,
token.address,
account4.address,
account5.address,
amount,
nonce,
expiry
);
const signature = ContractUtils.signMessage(account4, message);
await token.delegatedTransferWithFee(account4.address, account5.address, amount, expiry, signature);

const newBalance4 = await token.balanceOf(account4.address);
const newBalance5 = await token.balanceOf(account5.address);
const newBalanceFee = await token.balanceOf(feeAccount.address);

assert.deepStrictEqual(newBalanceFee.sub(oldBalanceFee), protocolFee);
assert.deepStrictEqual(newBalance5.sub(oldBalance5), amount.sub(protocolFee));
assert.deepStrictEqual(oldBalance4.sub(newBalance4), amount);
});
});
10 changes: 5 additions & 5 deletions packages/contracts/test/MultiSigToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ async function deployMultiSigWallet(
: undefined;
}

async function deployToken(deployer: Wallet, owner: string): Promise<LYT> {
async function deployToken(deployer: Wallet, owner: string, feeAccount: string): Promise<LYT> {
const factory = await ethers.getContractFactory("LYT");
const contract = (await factory.connect(deployer).deploy(owner)) as LYT;
const contract = (await factory.connect(deployer).deploy(owner, feeAccount)) as LYT;
await contract.deployed();
await contract.deployTransaction.wait();
return contract;
}

describe("Test for LYT token", () => {
const raws = HardhatAccount.keys.map((m) => new Wallet(m, ethers.provider));
const [deployer, account0, account1, account2, account3, account4] = raws;
const [deployer, feeAccount, account0, account1, account2, account3, account4] = raws;
const owners1 = [account0, account1, account2];

let multiSigFactory: MultiSigWalletFactory;
Expand Down Expand Up @@ -86,15 +86,15 @@ describe("Test for LYT token", () => {

it("Create Token, Owner is wallet", async () => {
const factory = await ethers.getContractFactory("LYT");
await expect(factory.connect(deployer).deploy(account0.address)).to.be.revertedWith(
await expect(factory.connect(deployer).deploy(account0.address, feeAccount.address)).to.be.revertedWith(
"function call to a non-contract account"
);
});

it("Create Token, Owner is MultiSigWallet", async () => {
assert.ok(multiSigWallet);

token = await deployToken(deployer, multiSigWallet.address);
token = await deployToken(deployer, multiSigWallet.address, feeAccount.address);
assert.deepStrictEqual(await token.getOwner(), multiSigWallet.address);
assert.deepStrictEqual(await token.balanceOf(multiSigWallet.address), BigNumber.from(0));
});
Expand Down
Loading