diff --git a/packages/contracts/contracts/Ledger.sol b/packages/contracts/contracts/Ledger.sol index a31fe810..fe8bb1c9 100644 --- a/packages/contracts/contracts/Ledger.sol +++ b/packages/contracts/contracts/Ledger.sol @@ -300,7 +300,7 @@ contract Ledger { bytes32 dataHash = keccak256(abi.encode(_phone, _account, nonce[_account])); require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), _signature) == _account, "Invalid signature"); address userAddress = linkCollection.toAddress(_phone); - require(userAddress != address(0x00), "Unregistered email-address"); + require(userAddress != address(0x00), "Unregistered phone-address"); require(userAddress == _account, "Invalid address"); require(unPayablePointBalances[_phone] > 0, "Insufficient balance"); @@ -327,7 +327,7 @@ contract Ledger { ); uint256 purchaseAmount = convertCurrencyToPoint(data.amount, data.currency); - uint256 feeAmount = convertCurrencyToPoint(data.amount * fee / 100, data.currency); + uint256 feeAmount = convertCurrencyToPoint((data.amount * fee) / 100, data.currency); uint256 feeToken = convertPointToToken(feeAmount); require(pointBalances[data.account] >= purchaseAmount + feeAmount, "Insufficient balance"); @@ -388,7 +388,7 @@ contract Ledger { ); uint256 purchaseAmount = convertCurrencyToPoint(data.amount, data.currency); - uint256 feeAmount = convertCurrencyToPoint(data.amount * fee / 100, data.currency); + uint256 feeAmount = convertCurrencyToPoint((data.amount * fee) / 100, data.currency); uint256 amountToken = convertPointToToken(purchaseAmount); uint256 feeToken = convertPointToToken(feeAmount); diff --git a/packages/contracts/contracts/ShopCollection.sol b/packages/contracts/contracts/ShopCollection.sol index ca0166e7..65fbdf53 100644 --- a/packages/contracts/contracts/ShopCollection.sol +++ b/packages/contracts/contracts/ShopCollection.sol @@ -2,12 +2,26 @@ pragma solidity ^0.8.0; +import "del-osx-artifacts/contracts/PhoneLinkCollection.sol"; import "./ValidatorCollection.sol"; /// @notice 상점컬랙션 contract ShopCollection { /// @notice Hash value of a blank string bytes32 public constant NULL = 0x32105b1d0b88ada155176b58ee08b45c31e4f2f7337475831982c313533b880c; + + /// @notice 검증자의 상태코드 + enum WithdrawStatus { + CLOSE, + OPEN + } + + struct WithdrawData { + uint256 amount; + address account; + WithdrawStatus status; + } + /// @notice 검증자의 상태코드 enum ShopStatus { INVALID, @@ -23,7 +37,9 @@ contract ShopCollection { uint256 providedPoint; // 제공된 포인트 총량 uint256 usedPoint; // 사용된 포인트 총량 uint256 settledPoint; // 정산된 포인트 총량 + uint256 withdrawnPoint; // 정산된 포인트 총량 ShopStatus status; + WithdrawData withdrawData; } mapping(string => ShopData) private shops; @@ -32,7 +48,10 @@ contract ShopCollection { string[] private items; address public validatorAddress; + address public linkCollectionAddress; + ValidatorCollection private validatorCollection; + PhoneLinkCollection private linkCollection; /// @notice 상점이 추가될 때 발생되는 이벤트 event AddedShop(string shopId, uint256 provideWaitTime, uint256 providePercent, bytes32 phone); @@ -45,15 +64,21 @@ contract ShopCollection { /// @notice 정산된 마일리가 증가할 때 발생되는 이벤트 event IncreasedSettledPoint(string shopId, uint256 increase, uint256 total, string purchaseId); + event OpenedWithdrawal(string shopId, uint256 amount, address account); + event ClosedWithdrawal(string shopId, uint256 amount, address account); + address public ledgerAddress; address public deployer; /// @notice 생성자 /// @param _validatorAddress 검증자컬랙션의 주소 - constructor(address _validatorAddress) { + constructor(address _validatorAddress, address _linkCollectionAddress) { validatorAddress = _validatorAddress; + linkCollectionAddress = _linkCollectionAddress; validatorCollection = ValidatorCollection(_validatorAddress); + linkCollection = PhoneLinkCollection(_linkCollectionAddress); + ledgerAddress = address(0x00); deployer = msg.sender; } @@ -102,7 +127,9 @@ contract ShopCollection { providedPoint: 0, usedPoint: 0, settledPoint: 0, - status: ShopStatus.ACTIVE + withdrawnPoint: 0, + status: ShopStatus.ACTIVE, + withdrawData: WithdrawData({ amount: 0, account: address(0x0), status: WithdrawStatus.CLOSE }) }); items.push(_shopId); shops[_shopId] = data; @@ -182,4 +209,50 @@ contract ShopCollection { function shopsLength() public view returns (uint256) { return items.length; } + + /// @notice 인출가능한 정산금액을 리턴한다. + /// @param _shopId 상점의 아이디 + function withdrawableOf(string memory _shopId) public view returns (uint256) { + ShopData memory shop = shops[_shopId]; + return shop.settledPoint - shop.withdrawnPoint; + } + + /// @notice 정산금의 인출을 요청한다. 상점주인만이 실행가능 + /// @param _shopId 상점아이디 + /// @param _amount 인출금 + function openWithdrawal(string calldata _shopId, uint256 _amount) public { + ShopData memory shop = shops[_shopId]; + + bytes32 phone = linkCollection.toPhone(msg.sender); + require(phone != bytes32(0x00), "Unregistered phone-address"); + require(shop.phone == phone, "Invalid address"); + + require(_amount <= shop.settledPoint - shop.withdrawnPoint, "Insufficient withdrawal amount"); + require(shop.withdrawData.status == WithdrawStatus.CLOSE, "Already opened"); + + shops[_shopId].withdrawData.account = msg.sender; + shops[_shopId].withdrawData.amount = _amount; + shops[_shopId].withdrawData.status = WithdrawStatus.OPEN; + + emit OpenedWithdrawal(_shopId, _amount, msg.sender); + } + + /// @notice 정산금의 인출을 마감한다. 상점주인만이 실행가능 + /// @param _shopId 상점아이디 + /// @param _amount 인출금 + function closeWithdrawal(string calldata _shopId, uint256 _amount) public { + ShopData memory shop = shops[_shopId]; + + bytes32 phone = linkCollection.toPhone(msg.sender); + require(phone != bytes32(0x00), "Unregistered phone-address"); + require(shop.phone == phone, "Invalid address"); + + require(shop.withdrawData.status == WithdrawStatus.OPEN, "Not opened"); + require(shop.withdrawData.amount == _amount, "Inconsistent amount"); + + shops[_shopId].withdrawData.status = WithdrawStatus.CLOSE; + shops[_shopId].withdrawnPoint += shop.withdrawData.amount; + + emit ClosedWithdrawal(_shopId, _amount, msg.sender); + } } diff --git a/packages/contracts/test/02-ShopCollection.test.ts b/packages/contracts/test/02-ShopCollection.test.ts index 457d5f74..2a5b7720 100644 --- a/packages/contracts/test/02-ShopCollection.test.ts +++ b/packages/contracts/test/02-ShopCollection.test.ts @@ -1,6 +1,6 @@ import { Amount } from "../src/utils/Amount"; import { ContractUtils } from "../src/utils/ContractUtils"; -import { ShopCollection, Token, ValidatorCollection } from "../typechain-types"; +import { PhoneLinkCollection, ShopCollection, Token, ValidatorCollection } from "../typechain-types"; import "@nomiclabs/hardhat-ethers"; import "@nomiclabs/hardhat-waffle"; @@ -19,7 +19,9 @@ describe("Test for ShopCollection", () => { provider.getWallets(); const validators = [validator1, validator2, validator3]; + const linkValidators = [validator1]; const shopWallets = [shop1, shop2, shop3, shop4, shop5]; + let linkCollectionContract: PhoneLinkCollection; let validatorContract: ValidatorCollection; let tokenContract: Token; let shopCollection: ShopCollection; @@ -27,6 +29,13 @@ describe("Test for ShopCollection", () => { const amount = Amount.make(20_000, 18); before(async () => { + const linkCollectionFactory = await hre.ethers.getContractFactory("PhoneLinkCollection"); + linkCollectionContract = (await linkCollectionFactory + .connect(deployer) + .deploy(linkValidators.map((m) => m.address))) as PhoneLinkCollection; + await linkCollectionContract.deployed(); + await linkCollectionContract.deployTransaction.wait(); + const tokenFactory = await hre.ethers.getContractFactory("Token"); tokenContract = (await tokenFactory.connect(deployer).deploy(deployer.address, "Sample", "SAM")) as Token; await tokenContract.deployed(); @@ -57,7 +66,7 @@ describe("Test for ShopCollection", () => { const shopCollectionFactory = await hre.ethers.getContractFactory("ShopCollection"); shopCollection = (await shopCollectionFactory .connect(deployer) - .deploy(validatorContract.address)) as ShopCollection; + .deploy(validatorContract.address, linkCollectionContract.address)) as ShopCollection; await shopCollection.deployed(); await shopCollection.deployTransaction.wait(); }); diff --git a/packages/contracts/test/03-Ledger.test.ts b/packages/contracts/test/03-Ledger.test.ts index 678912e7..879ce267 100644 --- a/packages/contracts/test/03-Ledger.test.ts +++ b/packages/contracts/test/03-Ledger.test.ts @@ -151,7 +151,7 @@ describe("Test for Ledger", () => { const shopCollectionFactory = await hre.ethers.getContractFactory("ShopCollection"); shopCollection = (await shopCollectionFactory .connect(deployer) - .deploy(validatorContract.address)) as ShopCollection; + .deploy(validatorContract.address, linkCollectionContract.address)) as ShopCollection; await shopCollection.deployed(); await shopCollection.deployTransaction.wait(); }; @@ -1697,6 +1697,64 @@ describe("Test for Ledger", () => { ); }); }); + + context("Withdrawal of settlement", () => { + const shopIndex = 2; + const shop = shopData[shopIndex]; + const amount2 = Amount.make(400, 18).value; + it("Check Settlement", async () => { + const withdrawalAmount = await shopCollection.withdrawableOf(shop.shopId); + expect(withdrawalAmount).to.equal(amount2); + }); + + it("Link phone-wallet of the shop", async () => { + const nonce = await linkCollectionContract.nonceOf(shopWallets[shopIndex].address); + const phoneHash = ContractUtils.getPhoneHash(shop.phone); + const signature = await ContractUtils.signRequestHash(shopWallets[shopIndex], phoneHash, nonce); + requestId = ContractUtils.getRequestId(phoneHash, shopWallets[shopIndex].address, nonce); + await expect( + linkCollectionContract + .connect(relay) + .addRequest(requestId, phoneHash, shopWallets[shopIndex].address, signature) + ) + .to.emit(linkCollectionContract, "AddedRequestItem") + .withArgs(requestId, phoneHash, shopWallets[shopIndex].address); + await linkCollectionContract.connect(validator1).voteRequest(requestId); + await linkCollectionContract.connect(validator1).countVote(requestId); + }); + + it("Open Withdrawal", async () => { + await expect( + shopCollection + .connect(shopWallets[shopIndex].connect(hre.waffle.provider)) + .openWithdrawal(shop.shopId, amount2) + ) + .to.emit(shopCollection, "OpenedWithdrawal") + .withNamedArgs({ + shopId: shop.shopId, + amount: amount2, + account: shopWallets[shopIndex].address, + }); + const withdrawalAmount = await shopCollection.withdrawableOf(shop.shopId); + expect(withdrawalAmount).to.equal(amount2); + }); + + it("Close Withdrawal", async () => { + await expect( + shopCollection + .connect(shopWallets[shopIndex].connect(hre.waffle.provider)) + .closeWithdrawal(shop.shopId, amount2) + ) + .to.emit(shopCollection, "ClosedWithdrawal") + .withNamedArgs({ + shopId: shop.shopId, + amount: amount2, + account: shopWallets[shopIndex].address, + }); + const withdrawalAmount = await shopCollection.withdrawableOf(shop.shopId); + expect(withdrawalAmount).to.equal(0); + }); + }); }); context("Multi Currency", () => {