diff --git a/contracts/fee/GardenFEEAccount.sol b/contracts/fee/GardenFEEAccount.sol index 3a7d8dd..92a5a97 100644 --- a/contracts/fee/GardenFEEAccount.sol +++ b/contracts/fee/GardenFEEAccount.sol @@ -26,7 +26,7 @@ contract GardenFEEAccount is EIP712Upgradeable { struct HTLC { bytes32 secretHash; - uint256 timeLock; + uint256 expiry; uint256 sendAmount; uint256 recieveAmount; } @@ -37,12 +37,12 @@ contract GardenFEEAccount is EIP712Upgradeable { keccak256( abi.encodePacked( "Claim(uint256 nonce,uint256 amount,HTLC[] htlcs)", - "HTLC(bytes32 secretHash,uint256 timeLock,uint256 sendAmount,uint256 recieveAmount)" + "HTLC(bytes32 secretHash,uint256 expiry,uint256 sendAmount,uint256 recieveAmount)" ) ); bytes32 private constant HTLC_TYPEHASH = - keccak256("HTLC(bytes32 secretHash,uint256 timeLock,uint256 sendAmount,uint256 recieveAmount)"); + keccak256("HTLC(bytes32 secretHash,uint256 expiry,uint256 sendAmount,uint256 recieveAmount)"); // Are set when the channel is created IERC20Upgradeable public token; @@ -54,10 +54,16 @@ contract GardenFEEAccount is EIP712Upgradeable { uint256 public amount; uint256 public nonce; uint256 public expiration; - uint256 public secretsProvided; + + mapping(bytes => uint256) public secretsClaimed; + mapping(bytes32 => bytes) public secrets; uint256 private constant TWO_DAYS = 2 * 7200; + function initialize() initializer external { + _disableInitializers(); + } + function __GardenFEEAccount_init( IERC20Upgradeable token_, address funder_, @@ -117,7 +123,7 @@ contract GardenFEEAccount is EIP712Upgradeable { * @param amount_ The amount of tokens to be claimed. * @param nonce_ The nonce value for the claim message. * @param htlcs The array of HTLCs in the claim. - * @param secrets The array of secrets corresponding to the HTLCs. + * @param secrets_ The array of secrets corresponding to the HTLCs. * @param funderSig The signature of the funder for the claim message. * @param recipientSig The signature of the recipient for the claim message. */ @@ -125,39 +131,52 @@ contract GardenFEEAccount is EIP712Upgradeable { uint256 amount_, uint256 nonce_, HTLC[] memory htlcs, - bytes[] memory secrets, + bytes[] memory secrets_, bytes memory funderSig, bytes memory recipientSig ) external { - require(htlcs.length == secrets.length, "GardenFEEAccount: invalid input"); + require(htlcs.length == secrets_.length, "FeeAccount: invalid input"); + require(!(htlcs.length > 0 && nonce_ == 0), "FeeAccount: zero nonce claim cannot contain htlcs"); bytes32 claimID = claimHash(amount_, nonce_, htlcs); - uint256 localSecretsProvided = 0; + if (nonce == nonce_ && expiration != 0) { + amount_ = amount; + } + + bool secretsProcessed = false; + for (uint256 i = 0; i < htlcs.length; i++) { - if (htlcs[i].timeLock > block.number && sha256(secrets[i]) == htlcs[i].secretHash) { - localSecretsProvided++; + if (secretsClaimed[secrets[htlcs[i].secretHash]] > 0) { + if (secretsClaimed[secrets[htlcs[i].secretHash]] != nonce_) { + secretsProcessed = true; + secretsClaimed[secrets[htlcs[i].secretHash]] = nonce_; + amount_ += htlcs[i].sendAmount; + amount_ -= htlcs[i].recieveAmount; + } + continue; + } + if (htlcs[i].timeLock > block.number && sha256(secrets_[i]) == htlcs[i].secretHash) { + secretsProcessed = true; + secretsClaimed[secrets_[i]] = nonce_; + secrets[htlcs[i].secretHash] = secrets_[i]; amount_ += htlcs[i].sendAmount; amount_ -= htlcs[i].recieveAmount; } } - require(amount_ <= totalAmount(), "GardenFEEAccount: invalid amount"); + require(amount_ <= totalAmount(), "FeeAccount: invalid amount"); if (expiration != 0) { // a claim exists, so should satisfy override conditions - require( - nonce_ > nonce || (nonce_ == nonce && localSecretsProvided > secretsProvided), - "GardenFEEAccount: override conditions not met" - ); + require(nonce_ > nonce || (nonce_ == nonce && secretsProcessed), "FeeAccount: override conditions not met"); } // verify funder and recipient signatures address funderSigner = claimID.recover(funderSig); address recipientSigner = claimID.recover(recipientSig); - require(funderSigner == funder, "GardenFEEAccount: invalid funder signature"); - require(recipientSigner == recipient, "GardenFEEAccount: invalid recipient signature"); + require(funderSigner == funder, "FeeAccount: invalid funder signature"); + require(recipientSigner == recipient, "FeeAccount: invalid recipient signature"); // update global claim state - secretsProvided = localSecretsProvided; expiration = block.number + TWO_DAYS; amount = amount_; nonce = nonce_; @@ -200,7 +219,7 @@ contract GardenFEEAccount is EIP712Upgradeable { abi.encode( HTLC_TYPEHASH, htlcs[i].secretHash, - htlcs[i].timeLock, + htlcs[i].expiry, htlcs[i].sendAmount, htlcs[i].recieveAmount ) diff --git a/contracts/fee/GardenFEEAccountFactory.sol b/contracts/fee/GardenFEEAccountFactory.sol index 31eacd8..b72f0a9 100644 --- a/contracts/fee/GardenFEEAccountFactory.sol +++ b/contracts/fee/GardenFEEAccountFactory.sol @@ -42,9 +42,13 @@ contract GardenFEEAccountFactory { token = token_; feeManager = feeManager_; - template = address(new GardenFEEAccount()); + feeAccountName = feeAccountName_; feeAccountVersion = feeAccountVersion_; + + GardenFEEAccount templateFeeAccount = new GardenFEEAccount(); + templateFeeAccount.initialize(); + template = address(templateFeeAccount); } /** diff --git a/contracts/htlc/GardenHTLC.sol b/contracts/htlc/GardenHTLC.sol index d674e66..3a5e831 100644 --- a/contracts/htlc/GardenHTLC.sol +++ b/contracts/htlc/GardenHTLC.sol @@ -24,7 +24,7 @@ contract GardenHTLC is EIP712 { address initiator; address redeemer; uint256 initiatedAt; - uint256 expiry; + uint256 timelock; uint256 amount; } @@ -33,7 +33,9 @@ contract GardenHTLC is EIP712 { mapping(bytes32 => Order) public orders; bytes32 private constant _INITIATE_TYPEHASH = - keccak256("Initiate(address redeemer,uint256 expiry,uint256 amount,bytes32 secretHash)"); + keccak256("Initiate(address redeemer,uint256 timelock,uint256 amount,bytes32 secretHash)"); + + bytes32 private constant _REFUND_TYPEHASH = keccak256("Refund(bytes32 orderId)"); event Initiated(bytes32 indexed orderID, bytes32 indexed secretHash, uint256 amount); event Redeemed(bytes32 indexed orderID, bytes32 indexed secretHash, bytes secret); @@ -43,19 +45,19 @@ contract GardenHTLC is EIP712 { * @notice . * @dev provides checks to ensure * 1. redeemer is not null address - * 3. expiry is greater than current block number + * 3. timelock is greater than 0 * 4. amount is not zero * @param redeemer public address of the reedeem - * @param expiry expiry in period for the htlc order + * @param timelock timelock in period for the htlc order * @param amount amount of tokens to trade */ modifier safeParams( address redeemer, - uint256 expiry, + uint256 timelock, uint256 amount ) { require(redeemer != address(0), "GardenHTLC: zero address redeemer"); - require(expiry > 0, "GardenHTLC: zero expiry"); + require(timelock > 0, "GardenHTLC: zero timelock"); require(amount > 0, "GardenHTLC: zero amount"); _; } @@ -70,17 +72,17 @@ contract GardenHTLC is EIP712 { * and sha256 hash should be used to support hashing methods on other non-evm chains. * Signers cannot generate orders with same secret hash or override an existing order. * @param redeemer public address of the redeemer - * @param expiry expiry in period for the htlc order + * @param timelock timelock in period for the htlc order * @param amount amount of tokens to trade * @param secretHash sha256 hash of the secret used for redemtion **/ function initiate( address redeemer, - uint256 expiry, + uint256 timelock, uint256 amount, bytes32 secretHash - ) external safeParams(redeemer, expiry, amount) { - _initiate(msg.sender, redeemer, expiry, amount, secretHash); + ) external safeParams(redeemer, timelock, amount) { + _initiate(msg.sender, redeemer, timelock, amount, secretHash); } /** @@ -89,23 +91,23 @@ contract GardenHTLC is EIP712 { * and sha256 hash should be used to support hashing methods on other non-evm chains. * Signers cannot generate orders with same secret hash or override an existing order. * @param redeemer public address of the redeemer - * @param expiry expiry in period for the htlc order + * @param timelock timelock in period for the htlc order * @param amount amount of tokens to trade * @param secretHash sha256 hash of the secret used for redemtion * @param signature EIP712 signature provided by authorized user for iniatiation. user will be assigned as initiator **/ function initiateWithSignature( address redeemer, - uint256 expiry, + uint256 timelock, uint256 amount, bytes32 secretHash, bytes calldata signature - ) external safeParams(redeemer, expiry, amount) { + ) external safeParams(redeemer, timelock, amount) { address initiator = _hashTypedDataV4( - keccak256(abi.encode(_INITIATE_TYPEHASH, redeemer, expiry, amount, secretHash)) + keccak256(abi.encode(_INITIATE_TYPEHASH, redeemer, timelock, amount, secretHash)) ).recover(signature); - _initiate(initiator, redeemer, expiry, amount, secretHash); + _initiate(initiator, redeemer, timelock, amount, secretHash); } /** @@ -134,7 +136,7 @@ contract GardenHTLC is EIP712 { } /** - * @notice Signers can refund the locked assets after expiry block number + * @notice Signers can refund the locked assets after timelock block number * @dev Signers cannot refund the an order before epiry block number or refund the same order * multiple times. * Funds will be SafeTransferred to the initiator. @@ -145,7 +147,7 @@ contract GardenHTLC is EIP712 { require(order.redeemer != address(0), "GardenHTLC: order not initiated"); require(!order.isFulfilled, "GardenHTLC: order fulfilled"); - require(order.initiatedAt + order.expiry < block.number, "GardenHTLC: order not expired"); + require(order.initiatedAt + order.timelock < block.number, "GardenHTLC: order not expired"); order.isFulfilled = true; @@ -164,13 +166,13 @@ contract GardenHTLC is EIP712 { * @param initiator_ The address of the initiator of the atomic swap * @param redeemer_ The address of the redeemer of the atomic swap * @param secretHash_ The hash of the secret used for redemption - * @param expiry_ The expiry block number for the atomic swap + * @param timelock_ The timelock block number for the atomic swap * @param amount_ The amount of tokens to be traded in the atomic swap */ function _initiate( address initiator_, address redeemer_, - uint256 expiry_, + uint256 timelock_, uint256 amount_, bytes32 secretHash_ ) internal { @@ -186,7 +188,7 @@ contract GardenHTLC is EIP712 { initiator: initiator_, redeemer: redeemer_, initiatedAt: block.number, - expiry: expiry_, + timelock: timelock_, amount: amount_ }); orders[orderID] = newOrder; @@ -195,4 +197,26 @@ contract GardenHTLC is EIP712 { token.safeTransferFrom(initiator_, address(this), orders[orderID].amount); } + + /** + * @notice Redeemers can let initiator refund the locked assets before expiry block number + * @dev Signers cannot refund the the same order multiple times. + * Funds will be SafeTransferred to the initiator. + * + * @param orderID orderID of the htlc order + * @param signature EIP712 signature provided by redeemer for instant refund. + */ + function instantRefund(bytes32 orderID, bytes calldata signature) external { + address redeemer = _hashTypedDataV4(keccak256(abi.encode(_REFUND_TYPEHASH, orderID))).recover(signature); + Order storage order = orders[orderID]; + + require(order.redeemer == redeemer, "HTLC: invalid redeemer signature"); + require(!order.isFulfilled, "HTLC: order fulfilled"); + + order.isFulfilled = true; + + emit Refunded(orderID); + + token.safeTransfer(order.initiator, order.amount); + } } diff --git a/contracts/stake/DelegateManager.sol b/contracts/stake/DelegateManager.sol index f6ecab1..0e03e0d 100644 --- a/contracts/stake/DelegateManager.sol +++ b/contracts/stake/DelegateManager.sol @@ -134,7 +134,7 @@ abstract contract DelegateManager is BaseStaker { require(stake.expiry < block.number, "DelegateManager: stake not expired"); uint8 multiplier = _calculateVoteMultiplier(newLockBlocks); - stake.expiry = block.number + newLockBlocks; + stake.expiry = multiplier == uint8(7) ? MAX_UINT_256 : block.number + newLockBlocks; stake.votes = multiplier * stake.units; stakes[stakeID] = stake; diff --git a/contracts/stake/FillerManager.sol b/contracts/stake/FillerManager.sol index df0f273..8aaae81 100644 --- a/contracts/stake/FillerManager.sol +++ b/contracts/stake/FillerManager.sol @@ -70,7 +70,9 @@ abstract contract FillerManager is BaseStaker { require(filler.deregisteredAt != 0, "FillerManager: not deregistered"); require(filler.deregisteredAt + FILLER_COOL_DOWN < block.number, "FillerManager: cooldown not passed"); - delete (fillers[filler_]); + fillers[filler_].feeInBips = 0; + fillers[filler_].stake = 0; + fillers[filler_].deregisteredAt = 0; SEED.safeTransfer(filler_, FILLER_STAKE); diff --git a/test/fee/gardenFeeAccount.test.ts b/test/fee/gardenFeeAccount.test.ts index 563f615..7baba3d 100644 --- a/test/fee/gardenFeeAccount.test.ts +++ b/test/fee/gardenFeeAccount.test.ts @@ -296,7 +296,7 @@ describe("--- Garden Fee Account ---", () => { it("User should not be able to claim with wrong number of secrets message.", async () => { const currentBlock = await ethers.provider.getBlockNumber(); claimMessage = { - nonce: 0, + nonce: 1, amount: ethers.parseEther("0.5"), htlcs: [ { @@ -353,7 +353,7 @@ describe("--- Garden Fee Account ---", () => { davidSignature, davidSignature ) - ).to.be.revertedWith("GardenFEEAccount: invalid amount"); + ).to.be.revertedWith("FeeAccount: invalid amount"); }); it("User should not be able to claim with wrong funder signature", async () => { await seed.transfer(davidGardenFEEAccountAddress, ethers.parseEther("1")); @@ -368,7 +368,7 @@ describe("--- Garden Fee Account ---", () => { davidSignature, davidSignature ) - ).to.be.revertedWith("GardenFEEAccount: invalid funder signature"); + ).to.be.revertedWith("FeeAccount: invalid funder signature"); }); it("User should not be able to claim with wrong user signature", async () => { await expect( @@ -382,7 +382,7 @@ describe("--- Garden Fee Account ---", () => { feeManagerSignature, feeManagerSignature ) - ).to.be.revertedWith("GardenFEEAccount: invalid recipient signature"); + ).to.be.revertedWith("FeeAccount: invalid recipient signature"); }); it("User should be able to claim few htlcs", async () => { await expect( @@ -410,7 +410,7 @@ describe("--- Garden Fee Account ---", () => { feeManagerSignature, davidSignature ) - ).to.be.revertedWith("GardenFEEAccount: override conditions not met"); + ).to.be.revertedWith("FeeAccount: override conditions not met"); }); it("User be able to claim with more number of secrets", async () => { await expect( @@ -429,7 +429,7 @@ describe("--- Garden Fee Account ---", () => { it("User be able to claim with greater nonce", async () => { const currentBlock = await ethers.provider.getBlockNumber(); claimMessage = { - nonce: 1, + nonce: 2, amount: ethers.parseEther("0.5"), htlcs: [ { @@ -470,7 +470,7 @@ describe("--- Garden Fee Account ---", () => { it("User be able to claim with greater nonce and amount equal to totalAmount", async () => { const currentBlock = await ethers.provider.getBlockNumber(); claimMessage = { - nonce: 2, + nonce: 3, amount: ethers.parseEther("0.5"), htlcs: [ { @@ -505,7 +505,7 @@ describe("--- Garden Fee Account ---", () => { it("User be able to claim with greater nonce and amount equal to 0", async () => { const currentBlock = await ethers.provider.getBlockNumber(); claimMessage = { - nonce: 3, + nonce: 4, amount: ethers.parseEther("0.5"), htlcs: [ { @@ -538,7 +538,7 @@ describe("--- Garden Fee Account ---", () => { ).to.emit(gardenFeeAccountFactory, "Claimed"); }); it("User should be able to settle after expiration.", async () => { - mine((await ethers.provider.getBlockNumber()) + 14400); + await mine((await ethers.provider.getBlockNumber()) + 14400); await expect(davidGardenFEEAccount.connect(david).settle()).to.emit( gardenFeeAccountFactory, "Closed" @@ -552,7 +552,7 @@ describe("--- Garden Fee Account ---", () => { let claimMessage: ClaimMessage; it("User should able to createAndClaim.", async () => { claimMessage = { - nonce: 0, + nonce: 1, amount: ethers.parseEther("1"), htlcs: [], }; diff --git a/test/htlc/gardenHTLC.test.ts b/test/htlc/gardenHTLC.test.ts index 132bbf7..9c246fb 100644 --- a/test/htlc/gardenHTLC.test.ts +++ b/test/htlc/gardenHTLC.test.ts @@ -21,7 +21,7 @@ describe("--- HTLC ---", () => { secretHash: BytesLike; }; - const TYPES: Record = { + const INITIATE_TYPE: Record = { Initiate: [ { name: "redeemer", type: "address" }, { name: "expiry", type: "uint256" }, @@ -30,6 +30,10 @@ describe("--- HTLC ---", () => { ], }; + const REFUND_TYPE: Record = { + Refund: [{ name: "orderId", type: "bytes32" }], + }; + let owner: HardhatEthersSigner; let alice: HardhatEthersSigner; let bob: HardhatEthersSigner; @@ -45,12 +49,14 @@ describe("--- HTLC ---", () => { let secret3: BytesLike; let secret4: BytesLike; let secret5: BytesLike; + let secret6: BytesLike; let orderID1: BytesLike; let orderID2: BytesLike; let orderID3: BytesLike; let orderID4: BytesLike; let orderID5: BytesLike; + let orderID6: BytesLike; let expiry_: BigNumberish; @@ -74,6 +80,7 @@ describe("--- HTLC ---", () => { secret3 = randomBytes(32); secret4 = randomBytes(32); secret5 = randomBytes(32); + secret6 = randomBytes(32); }); describe("- Pre-Conditions -", () => { @@ -359,7 +366,7 @@ describe("--- HTLC ---", () => { secretHash: ethers.sha256(secret5), }; - const signature = await bob.signTypedData(DOMAIN, TYPES, initiate); + const signature = await bob.signTypedData(DOMAIN, INITIATE_TYPE, initiate); await expect( gardenHTLC @@ -383,7 +390,7 @@ describe("--- HTLC ---", () => { secretHash: ethers.sha256(secret5), }; - const signature = await alice.signTypedData(DOMAIN, TYPES, initiate); + const signature = await alice.signTypedData(DOMAIN, INITIATE_TYPE, initiate); await expect( gardenHTLC @@ -415,7 +422,7 @@ describe("--- HTLC ---", () => { ) ); - const signature = await alice.signTypedData(DOMAIN, TYPES, initiate); + const signature = await alice.signTypedData(DOMAIN, INITIATE_TYPE, initiate); await expect( gardenHTLC @@ -533,4 +540,61 @@ describe("--- HTLC ---", () => { expect(await seed.balanceOf(alice.address)).to.equal(ethers.parseEther("500")); }); }); + + describe("- HTLC - Instant Refund -", () => { + let instantRefund: { + orderId: string; + }; + it("Should not able to instant refund a swap with an invalid signature.", async () => { + expiry_ = (await ethers.provider.getBlockNumber()) + 7200; + const initiate: Initiate = { + redeemer: bob.address, + expiry: expiry_, + amount: ethers.parseEther("100"), + secretHash: ethers.sha256(secret6), + }; + + orderID6 = ethers.sha256( + ethers.AbiCoder.defaultAbiCoder().encode( + ["bytes32", "address"], + [initiate.secretHash, alice.address] + ) + ); + await expect( + gardenHTLC + .connect(alice) + .initiate( + initiate.redeemer, + initiate.expiry, + initiate.amount, + initiate.secretHash + ) + ) + .to.emit(gardenHTLC, "Initiated") + .withArgs(orderID6, initiate.secretHash, initiate.amount); + + instantRefund = { orderId: orderID6 }; + + const instantRefundSig = await alice.signTypedData( + DOMAIN, + REFUND_TYPE, + instantRefund + ); + + await expect( + gardenHTLC.connect(charlie).instantRefund(orderID6, instantRefundSig) + ).to.be.revertedWith("HTLC: invalid redeemer signature"); + }); + it("Should be able to instant refund a swap with an valid signature.", async () => { + const instantRefundSig = await bob.signTypedData( + DOMAIN, + REFUND_TYPE, + instantRefund + ); + + await expect(gardenHTLC.connect(charlie).instantRefund(orderID6, instantRefundSig)) + .to.emit(gardenHTLC, "Refunded") + .withArgs(orderID6); + }); + }); });