Skip to content

Commit

Permalink
Add a transfer function that charges a fee
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelKim20 committed Jul 6, 2024
1 parent 609bf58 commit 9563002
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 23 deletions.
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

0 comments on commit 9563002

Please sign in to comment.