diff --git a/packages/contracts/contracts/BIP20/BIP20DelegatedTransfer.sol b/packages/contracts/contracts/BIP20/BIP20DelegatedTransfer.sol index 29db565..e56b2ee 100644 --- a/packages/contracts/contracts/BIP20/BIP20DelegatedTransfer.sol +++ b/packages/contracts/contracts/BIP20/BIP20DelegatedTransfer.sol @@ -11,12 +11,29 @@ contract BIP20DelegatedTransfer is BIP20, IBIP20DelegatedTransfer { /* * Storage */ + address internal owner; + mapping(address => uint256) internal nonce; + address public protocolFeeAccount; + + uint256 internal protocolFee; + + uint256 public constant MAX_PROTOCOL_FEE = 5e18; + /* * Public functions */ - constructor(string memory name_, string memory symbol_) BIP20(name_, symbol_) {} + constructor( + string memory name_, + string memory symbol_, + address account_, + address feeAccount_ + ) BIP20(name_, symbol_) { + owner = account_; + protocolFeeAccount = feeAccount_; + protocolFee = 1e17; + } function nonceOf(address account) external view override returns (uint256) { return nonce[account]; @@ -30,11 +47,55 @@ 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]++; return true; } + + function getProtocolFee() external view override returns (uint256) { + return protocolFee; + } + + function changeProtocolFee(uint256 _protocolFee) external override { + require(msg.sender == owner, "BIP20DelegatedTransfer: Sender is not authorized to execute."); + require( + _protocolFee <= BIP20DelegatedTransfer.MAX_PROTOCOL_FEE, + "LoyaltyToken: The value entered is not an appropriate value." + ); + protocolFee = _protocolFee; + } + + function changeProtocolFeeAccount(address _account) external override { + require(msg.sender == protocolFeeAccount, "BIP20DelegatedTransfer: Sender is not authorized to execute."); + + protocolFeeAccount = _account; + } + + function delegatedTransferWithFee( + address from, + address to, + uint256 amount, + uint256 expiry, + 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, + "BIP20DelegatedTransfer: Invalid signature" + ); + require(expiry > block.timestamp, "BIP20DelegatedTransfer: Expired signature"); + + require(amount > protocolFee, "BIP20DelegatedTransfer: The amount should be greater than the fee."); + require(balanceOf(from) >= amount, "BIP20DelegatedTransfer: transfer amount exceeds balance"); + super._transfer(from, to, amount - protocolFee); + super._transfer(from, protocolFeeAccount, protocolFee); + nonce[from]++; + return true; + } } diff --git a/packages/contracts/contracts/BIP20/IBIP20DelegatedTransfer.sol b/packages/contracts/contracts/BIP20/IBIP20DelegatedTransfer.sol index 5498e2a..4d421f7 100644 --- a/packages/contracts/contracts/BIP20/IBIP20DelegatedTransfer.sol +++ b/packages/contracts/contracts/BIP20/IBIP20DelegatedTransfer.sol @@ -13,4 +13,14 @@ interface IBIP20DelegatedTransfer is IBIP20 { uint256 expiry, bytes calldata signature ) external returns (bool); + function delegatedTransferWithFee( + address from, + address to, + uint256 amount, + uint256 expiry, + bytes calldata signature + ) external returns (bool); + function getProtocolFee() external view returns (uint256); + function changeProtocolFee(uint256 _protocolFee) external; + function changeProtocolFeeAccount(address _account) external; } diff --git a/packages/contracts/contracts/LYT.sol b/packages/contracts/contracts/LYT.sol index a878e45..e9eb230 100644 --- a/packages/contracts/contracts/LYT.sol +++ b/packages/contracts/contracts/LYT.sol @@ -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_) {} } diff --git a/packages/contracts/contracts/LoyaltyToken.sol b/packages/contracts/contracts/LoyaltyToken.sol index 67430d5..6e78b34 100644 --- a/packages/contracts/contracts/LoyaltyToken.sol +++ b/packages/contracts/contracts/LoyaltyToken.sol @@ -11,7 +11,6 @@ contract LoyaltyToken is BIP20DelegatedTransfer { /* * Storage */ - address internal owner; /* * Modifiers @@ -24,11 +23,15 @@ contract LoyaltyToken is BIP20DelegatedTransfer { /* * Public functions */ - constructor(string memory name_, string memory symbol_, address account_) BIP20DelegatedTransfer(name_, symbol_) { - owner = account_; + constructor( + string memory name_, + string memory symbol_, + address account_, + address feeAccount_ + ) BIP20DelegatedTransfer(name_, symbol_, account_, feeAccount_) { require( IMultiSigWallet(owner).supportsInterface(type(IMultiSigWallet).interfaceId), - "Invalid interface ID of multi sig wallet" + "LoyaltyToken: Invalid interface ID of multi sig wallet" ); } diff --git a/packages/contracts/deploy/main_chain_devnet/deploy.ts b/packages/contracts/deploy/main_chain_devnet/deploy.ts index 57b4062..d2112c5 100644 --- a/packages/contracts/deploy/main_chain_devnet/deploy.ts +++ b/packages/contracts/deploy/main_chain_devnet/deploy.ts @@ -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; class Deployments { public deployments: Map; @@ -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, }; } @@ -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(); diff --git a/packages/contracts/deploy/side_chain_devnet/deploy.ts b/packages/contracts/deploy/side_chain_devnet/deploy.ts index 30c15dd..12fa057 100644 --- a/packages/contracts/deploy/side_chain_devnet/deploy.ts +++ b/packages/contracts/deploy/side_chain_devnet/deploy.ts @@ -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; class Deployments { public deployments: Map; @@ -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, }; } @@ -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(); diff --git a/packages/contracts/env/.env.sample b/packages/contracts/env/.env.sample index 04c993d..14fe59b 100644 --- a/packages/contracts/env/.env.sample +++ b/packages/contracts/env/.env.sample @@ -5,4 +5,7 @@ TEST_NET_URL=https://testnet.bosagora.org # 0x02eaFC1091533F984dB53483a7215c7a982a3Ac1 DEPLOYER=0xdf29fb48bf34751707572533b6d6b4544e9ca1efb4a1fce9442164b575c0c061 +# 0x3633B7eBd5562316BD3740FAe1d5A4aD46DbD8f0 +PROTOCOL_FEE=0xf077a67e3982b1fba2233cfec2f22680bf090f91e0b0f494ed557b31b5bf4b9f + REPORT_GAS=true diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index 2f21782..ba2bfe6 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -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); } diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 78bdc50..6095f4e 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "loyalty-tokens", - "version": "2.0.0", + "version": "2.1.1", "description": "Smart contracts for the loyalty tokens", "files": [ "**/*.sol" diff --git a/packages/contracts/test/DelegatedTransfer.test.ts b/packages/contracts/test/DelegatedTransfer.test.ts index ac9298c..ff262ad 100644 --- a/packages/contracts/test/DelegatedTransfer.test.ts +++ b/packages/contracts/test/DelegatedTransfer.test.ts @@ -42,9 +42,9 @@ async function deployMultiSigWallet( : undefined; } -async function deployToken(deployer: Wallet, owner: string): Promise { +async function deployToken(deployer: Wallet, owner: string, feeAccount: string): Promise { 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; @@ -52,7 +52,7 @@ async function deployToken(deployer: Wallet, owner: string): Promise { 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; @@ -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)); }); @@ -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 () => { @@ -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 () => { @@ -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); + }); }); diff --git a/packages/contracts/test/MultiSigToken.test.ts b/packages/contracts/test/MultiSigToken.test.ts index c8908ac..8cf8dea 100644 --- a/packages/contracts/test/MultiSigToken.test.ts +++ b/packages/contracts/test/MultiSigToken.test.ts @@ -41,9 +41,9 @@ async function deployMultiSigWallet( : undefined; } -async function deployToken(deployer: Wallet, owner: string): Promise { +async function deployToken(deployer: Wallet, owner: string, feeAccount: string): Promise { 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; @@ -51,7 +51,7 @@ async function deployToken(deployer: Wallet, owner: string): Promise { 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; @@ -86,7 +86,7 @@ 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" ); }); @@ -94,7 +94,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)); });