-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add batch deposit function and ClaimInterest contract
- Loading branch information
1 parent
cef4ec0
commit cc5ca5b
Showing
3 changed files
with
139 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
pragma solidity ^0.8.0; | ||
|
||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
|
||
/** | ||
* @title ClaimInterest | ||
* @dev This contract allows users to claim their interest. | ||
* The interest claim is authenticated by a signature from a trusted signer. | ||
* The owner of the contract can change the signer. | ||
*/ | ||
contract ClaimInterest is Ownable { | ||
using SafeERC20 for IERC20; | ||
using ECDSA for bytes32; | ||
|
||
address public signer; | ||
IERC20 public token; | ||
mapping(address => uint256) public nonces; | ||
|
||
constructor(address _owner, address _signer, address _token) Ownable(_owner) { | ||
signer = _signer; | ||
token = IERC20(_token); | ||
} | ||
|
||
/** | ||
* @notice Claim the interest amount. | ||
* @dev The claim is authenticated by a signature from the trusted signer. | ||
* @param interestAmount The amount of interest to claim. | ||
* @param signature The signature from the signer. | ||
*/ | ||
function claimInterest(uint256 interestAmount, uint256 nonce, bytes memory signature) public { | ||
require(nonce == nonces[msg.sender], "Invalid nonce"); | ||
bytes32 message = keccak256(abi.encodePacked(msg.sender, interestAmount, nonce)); | ||
bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(message); | ||
require(ethSignedMessageHash.recover(signature) == signer, "Invalid signature"); | ||
nonces[msg.sender] += 1; // Increment nonce for the user | ||
token.safeTransfer(msg.sender, interestAmount); | ||
} | ||
|
||
/** | ||
* @notice Change the trusted signer. | ||
* @dev Only the owner can change the signer. | ||
* @param newSigner The address of the new signer. | ||
*/ | ||
function changeSigner(address newSigner) public onlyOwner { | ||
require(newSigner != address(0), "Invalid address"); | ||
signer = newSigner; | ||
} | ||
|
||
/** | ||
* @notice Admin function to force increment a user's nonce | ||
* @dev signer can increment a user's nonce to invalidate previous signatures | ||
* @param user The address of the user nonce to change. | ||
*/ | ||
function incrementNonce(address user) public { | ||
require(msg.sender == signer, "Only signer can change user nonce"); | ||
nonces[user] += 1; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.20; | ||
|
||
import "forge-std/Test.sol"; | ||
import "@source/automation/ClaimInterest.sol"; | ||
import "@source/dev/tokens/TokenERC20.sol"; | ||
|
||
contract ClaimInterestTest is Test { | ||
ClaimInterest claimInterest; | ||
address owner; | ||
uint256 ownerKey; | ||
address signer; | ||
uint256 signerKey; | ||
TokenERC20 token; | ||
|
||
function setUp() public { | ||
(owner, ownerKey) = makeAddrAndKey("owner"); | ||
(signer, signerKey) = makeAddrAndKey("signer"); | ||
vm.startBroadcast(ownerKey); | ||
token = new TokenERC20(6); | ||
claimInterest = new ClaimInterest(owner, signer, address(token)); | ||
// deposit interest to contract | ||
token.transfer(address(claimInterest), uint256(10000e6)); | ||
vm.stopBroadcast(); | ||
} | ||
|
||
function test_claim_interest() public { | ||
(address user, uint256 userKey) = makeAddrAndKey("user"); | ||
// test user can claim 100 usdc interest | ||
uint256 userNonce = claimInterest.nonces(user); | ||
bytes32 message = keccak256(abi.encodePacked(user, uint256(100e6), userNonce)); | ||
bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(message); | ||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, ethSignedMessageHash); | ||
bytes memory signature = abi.encodePacked(r, s, v); | ||
vm.startBroadcast(userKey); | ||
assertEq(token.balanceOf(user), 0); | ||
claimInterest.claimInterest(100e6, userNonce, signature); | ||
assertEq(token.balanceOf(user), 100e6); | ||
// should not be able to claim again with the same nonce | ||
vm.expectRevert(); | ||
claimInterest.claimInterest(100e6, userNonce, signature); | ||
vm.stopBroadcast(); | ||
} | ||
|
||
function test_invalidate_nonce() public { | ||
(address user, uint256 userKey) = makeAddrAndKey("user"); | ||
uint256 userNonce = claimInterest.nonces(user); | ||
vm.prank(signer); | ||
// force increment nonce | ||
claimInterest.incrementNonce(user); | ||
bytes32 message = keccak256(abi.encodePacked(user, uint256(100e6), userNonce)); | ||
bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(message); | ||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, ethSignedMessageHash); | ||
bytes memory signature = abi.encodePacked(r, s, v); | ||
vm.startBroadcast(userKey); | ||
assertEq(token.balanceOf(user), 0); | ||
vm.expectRevert(); | ||
claimInterest.claimInterest(100e6, userNonce, signature); | ||
vm.stopBroadcast(); | ||
} | ||
|
||
function test_change_signer() public { | ||
vm.startBroadcast(ownerKey); | ||
address oldSigner = claimInterest.signer(); | ||
assertEq(oldSigner, signer); | ||
address newSigner = makeAddr("newSigner"); | ||
claimInterest.changeSigner(newSigner); | ||
address currentSigner = claimInterest.signer(); | ||
assertEq(currentSigner, newSigner); | ||
} | ||
} |