Skip to content

Commit

Permalink
Add batch deposit function and ClaimInterest contract
Browse files Browse the repository at this point in the history
  • Loading branch information
CodeExplorer29 committed Apr 1, 2024
1 parent cef4ec0 commit cc5ca5b
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 0 deletions.
7 changes: 7 additions & 0 deletions contracts/automation/AaveUsdcSaveAutomation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
61 changes: 61 additions & 0 deletions contracts/automation/ClaimInterest.sol
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;
}
}
71 changes: 71 additions & 0 deletions test/automation/ClaimInterest.t.sol
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);
}
}

0 comments on commit cc5ca5b

Please sign in to comment.