From cc5ca5b96862e02da4f099aecc23b93f816e8cf3 Mon Sep 17 00:00:00 2001 From: "david.ding" Date: Mon, 1 Apr 2024 16:08:05 +0800 Subject: [PATCH] Add batch deposit function and ClaimInterest contract --- .../automation/AaveUsdcSaveAutomation.sol | 7 ++ contracts/automation/ClaimInterest.sol | 61 ++++++++++++++++ test/automation/ClaimInterest.t.sol | 71 +++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 contracts/automation/ClaimInterest.sol create mode 100644 test/automation/ClaimInterest.t.sol diff --git a/contracts/automation/AaveUsdcSaveAutomation.sol b/contracts/automation/AaveUsdcSaveAutomation.sol index 4b339ad..9cccf3f 100644 --- a/contracts/automation/AaveUsdcSaveAutomation.sol +++ b/contracts/automation/AaveUsdcSaveAutomation.sol @@ -35,6 +35,13 @@ contract AaveUsdcSaveAutomation is Ownable { emit UsdcDepositedToAave(_user, amount); } + function depositUsdcToAaveBatch(address[] calldata _users, uint256[] calldata amounts) public onlyBot { + require(_users.length == amounts.length, "invalid input"); + for (uint256 i = 0; i < _users.length; i++) { + depositUsdcToAave(_users[i], amounts[i]); + } + } + function addBot(address bot) public onlyOwner { bots[bot] = true; emit BotAdded(bot); diff --git a/contracts/automation/ClaimInterest.sol b/contracts/automation/ClaimInterest.sol new file mode 100644 index 0000000..d7d8486 --- /dev/null +++ b/contracts/automation/ClaimInterest.sol @@ -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; + } +} diff --git a/test/automation/ClaimInterest.t.sol b/test/automation/ClaimInterest.t.sol new file mode 100644 index 0000000..3576338 --- /dev/null +++ b/test/automation/ClaimInterest.t.sol @@ -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); + } +}