In my previous article Generating Unit Tests From Kode Sherpa I discussed about unit tests generation and generated unit tests as approx 9 tests were failing so in this article we will now modify the test to foundry and then compile in foundry so that we can analyze that if the generated smart contract is adhering error free compilation from Kode Sherpa.
The test suite created looking at hardhat in foundry
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
// --------------------------------------------------
// Minimal MockERC20 (self-contained)
// --------------------------------------------------
contract MockERC20 {
string public name = "MockToken";
string public symbol = "MTK";
uint8 public decimals = 18;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
function mint(address to, uint256 amount) external {
balanceOf[to] += amount;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
require(balanceOf[msg.sender] >= amount, "balance too low");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
require(balanceOf[from] >= amount, "balance too low");
require(allowance[from][msg.sender] >= amount, "allowance too low");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
}
// --------------------------------------------------
// Import your staking contract
// --------------------------------------------------
import "../src/FixedAprSingleTokenStakingPullPayments.sol";
// --------------------------------------------------
// Test Contract
// --------------------------------------------------
contract FixedAprSingleTokenStakingTest is Test {
MockERC20 stakingToken;
FixedAprSingleTokenStakingPullPayments staking;
address alice = address(1);
address bob = address(2);
uint256 stakeAmount = 1000 ether;
uint256 stakeAmount2 = 250 ether;
uint256 mintAmount = 1_000_000 ether;
function setUp() public {
stakingToken = new MockERC20();
staking = new FixedAprSingleTokenStakingPullPayments(IERC20(address(stakingToken)));
stakingToken.mint(alice, mintAmount);
stakingToken.mint(bob, mintAmount);
}
// ----------------------------
// Constructor
// ----------------------------
function testConstructor() public {
assertEq(address(staking.stakingToken()), address(stakingToken));
assertEq(staking.aprBps(), 500);
assertEq(staking.totalStaked(), 0);
}
function testConstructorRevertZeroAddress() public {
vm.expectRevert("staking token is zero");
new FixedAprSingleTokenStakingPullPayments(IERC20(address(0)));
}
// ----------------------------
// Stake
// ----------------------------
function testStake() public {
vm.startPrank(alice);
stakingToken.approve(address(staking), stakeAmount);
staking.stake(stakeAmount);
assertEq(staking.stakedBalance(alice), stakeAmount);
assertEq(staking.totalStaked(), stakeAmount);
vm.stopPrank();
}
function testStakeRevertZero() public {
vm.prank(alice);
vm.expectRevert("amount=0");
staking.stake(0);
}
function testStakeAccrual() public {
vm.startPrank(alice);
stakingToken.approve(address(staking), stakeAmount + stakeAmount2);
staking.stake(stakeAmount);
vm.warp(block.timestamp + 30 days);
staking.stake(stakeAmount2);
assertGt(staking.owedBalance(alice), 0);
vm.stopPrank();
}
// ----------------------------
// Unstake
// ----------------------------
function testUnstake() public {
vm.startPrank(alice);
stakingToken.approve(address(staking), stakeAmount);
staking.stake(stakeAmount);
staking.unstake(stakeAmount2);
assertEq(staking.stakedBalance(alice), stakeAmount - stakeAmount2);
assertEq(staking.owedBalance(alice), stakeAmount2);
vm.stopPrank();
}
function testUnstakeWithRewards() public {
vm.startPrank(alice);
stakingToken.approve(address(staking), stakeAmount);
staking.stake(stakeAmount);
vm.warp(block.timestamp + 60 days);
staking.unstake(stakeAmount2);
assertGt(staking.owedBalance(alice), stakeAmount2);
vm.stopPrank();
}
// ----------------------------
// Withdraw
// ----------------------------
function testWithdrawPrincipal() public {
vm.startPrank(alice);
stakingToken.approve(address(staking), stakeAmount);
staking.stake(stakeAmount);
staking.unstake(stakeAmount2);
uint256 owed = staking.owedBalance(alice);
staking.withdraw();
assertEq(staking.owedBalance(alice), 0);
assertEq(stakingToken.balanceOf(alice), mintAmount - stakeAmount + owed);
vm.stopPrank();
}
function testWithdrawRewards() public {
vm.startPrank(alice);
stakingToken.approve(address(staking), stakeAmount);
staking.stake(stakeAmount);
vm.warp(block.timestamp + 90 days);
uint256 beforeBal = stakingToken.balanceOf(alice);
staking.withdraw();
uint256 afterBal = stakingToken.balanceOf(alice);
assertGt(afterBal, beforeBal);
vm.stopPrank();
}
function testWithdrawRevert() public {
vm.prank(alice);
vm.expectRevert("nothing owed");
staking.withdraw();
}
// ----------------------------
// View functions
// ----------------------------
function testPendingRewardsGrowth() public {
vm.startPrank(alice);
stakingToken.approve(address(staking), stakeAmount + stakeAmount2);
staking.stake(stakeAmount);
vm.warp(block.timestamp + 10 days);
staking.stake(stakeAmount2);
vm.warp(block.timestamp + 5 days);
uint256 p1 = staking.pendingRewards(alice);
vm.warp(block.timestamp + 5 days);
uint256 p2 = staking.pendingRewards(alice);
assertGt(p1, 0);
assertGt(p2, p1);
vm.stopPrank();
}
function testWithdrawable() public {
vm.startPrank(alice);
stakingToken.approve(address(staking), stakeAmount);
staking.stake(stakeAmount);
staking.unstake(stakeAmount2);
assertEq(staking.withdrawable(alice), stakeAmount2);
vm.stopPrank();
}
// ----------------------------
// Multi-user
// ----------------------------
function testMultiUser() public {
vm.startPrank(alice);
stakingToken.approve(address(staking), stakeAmount);
staking.stake(stakeAmount);
vm.stopPrank();
vm.startPrank(bob);
stakingToken.approve(address(staking), stakeAmount2);
staking.stake(stakeAmount2);
vm.stopPrank();
vm.warp(block.timestamp + 45 days);
vm.prank(alice);
staking.unstake(stakeAmount2);
assertGt(staking.owedBalance(alice), stakeAmount2);
assertEq(staking.owedBalance(bob), 0);
}
}
Now based on the above test suite I ran the foundry test and this was my output
To further check if my test is utilizing 100 percent coverage so I ran forge coverage and this was my result
Based on this result I can fairly conclude that the below generated contract from Kode Sherpa is exhibiting 100 percent coverage for contract and test as well.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title FixedAprSingleTokenStakingPullPayments
* @author akb
* @notice Single-token ERC20 staking with fixed immutable APR rewards (in bps) and pull-payment withdrawals.
* - Stake/unstake only update accounting (no user transfers).
* - withdraw() is the only function that transfers tokens to users (principal + rewards).
*/
contract FixedAprSingleTokenStakingPullPayments is ReentrancyGuard {
using SafeERC20 for IERC20;
/**
* @notice ERC20 token that users stake and in which rewards are paid.
*/
IERC20 public immutable stakingToken;
/**
* @notice Basis points denominator used for APR calculations.
* @dev 10,000 bps = 100%.
*/
uint256 public constant RATE_DENOMINATOR = 10_000;
/**
* @notice Number of seconds used as one year for APR calculations.
* @dev Fixed to 365 days.
*/
uint256 public constant YEAR = 365 days;
/**
* @notice Fixed APR expressed in basis points (bps).
* @dev Immutable and set in the constructor. Example: 500 bps = 5.00% APR.
*/
uint256 public immutable aprBps;
/**
* @notice Amount of principal currently staked by each user.
*/
mapping(address => uint256) public stakedBalance;
/**
* @notice Pull-payment balance owed to each user (principal moved out + accrued rewards).
*/
mapping(address => uint256) public owedBalance;
/**
* @notice Last timestamp at which rewards were accrued/settled for each user.
*/
mapping(address => uint256) public lastAccrualTime;
/**
* @notice Total principal staked across all users.
*/
uint256 public totalStaked;
// Events (must match tests exactly)
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount, uint256 creditedToOwed);
event RewardsAccrued(address indexed user, uint256 reward, uint256 fromTime, uint256 toTime);
event Withdrawn(address indexed user, uint256 amount);
/**
* @notice Creates the staking contract for a given ERC20 token.
* @param _stakingToken The ERC20 token to be staked and used for reward payouts.
*/
constructor(IERC20 _stakingToken) {
require(address(_stakingToken) != address(0), "staking token is zero");
stakingToken = _stakingToken;
// Immutable APR set to 5.00% (500 bps)
aprBps = 500;
}
/**
* @notice Stake tokens. Requires prior approval.
* @dev Settles rewards up to now before changing principal.
* @param amount The amount of tokens to stake.
*/
function stake(uint256 amount) external nonReentrant {
require(amount > 0, "amount=0");
// Must initialize lastAccrualTime on first stake (via _accrue)
_accrue(msg.sender);
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
stakedBalance[msg.sender] += amount;
totalStaked += amount;
emit Staked(msg.sender, amount);
}
/**
* @notice Unstake tokens (principal). Does NOT transfer tokens to the user.
* @dev Settles rewards up to now, then moves unstaked principal into owedBalance (pull pattern).
* @param amount The amount of principal to unstake.
*/
function unstake(uint256 amount) external nonReentrant {
require(amount > 0, "amount=0");
_accrue(msg.sender);
require(stakedBalance[msg.sender] >= amount, "insufficient staked");
stakedBalance[msg.sender] -= amount;
totalStaked -= amount;
owedBalance[msg.sender] += amount;
emit Unstaked(msg.sender, amount, amount);
}
/**
* @notice Withdraw owed tokens (principal + rewards).
* @dev This is the ONLY function that transfers tokens to users.
*/
function withdraw() external nonReentrant {
_accrue(msg.sender);
uint256 owed = owedBalance[msg.sender];
require(owed > 0, "nothing owed");
owedBalance[msg.sender] = 0;
stakingToken.safeTransfer(msg.sender, owed);
emit Withdrawn(msg.sender, owed);
}
/**
* @notice View function to estimate pending rewards since last accrual (not yet credited to owedBalance).
* @param user The address to query.
* @return The amount of rewards accrued since the last checkpoint.
*/
function pendingRewards(address user) external view returns (uint256) {
uint256 principal = stakedBalance[user];
if (principal == 0) return 0;
uint256 last = lastAccrualTime[user];
if (last == 0) return 0;
uint256 elapsed = block.timestamp - last;
if (elapsed == 0) return 0;
return (principal * aprBps * elapsed) / YEAR / RATE_DENOMINATOR;
}
/**
* @notice View function for total withdrawable amount (already owed; does not include unaccrued pending rewards).
*/
function withdrawable(address user) external view returns (uint256) {
return owedBalance[user];
}
/**
* @dev Accrues rewards for `user` from lastAccrualTime to now and credits them to owedBalance.
* Emits RewardsAccrued when reward > 0 with fromTime=last and toTime=block.timestamp.
*/
function _accrue(address user) internal {
uint256 last = lastAccrualTime[user];
// Initialize checkpoint on first interaction; no retroactive rewards.
if (last == 0) {
lastAccrualTime[user] = block.timestamp;
return;
}
uint256 principal = stakedBalance[user];
uint256 toTime = block.timestamp;
if (toTime <= last) return;
// No principal: just move checkpoint forward.
if (principal == 0) {
lastAccrualTime[user] = toTime;
return;
}
uint256 elapsed = toTime - last;
uint256 reward = (principal * aprBps * elapsed) / YEAR / RATE_DENOMINATOR;
lastAccrualTime[user] = toTime;
if (reward > 0) {
owedBalance[user] += reward;
emit RewardsAccrued(user, reward, last, toTime);
}
}
}
The only tweak I did was introducing ERC20 interfaces in the contract and then adding reentracny guard properly pointing towards util folder rather than security folder.
In the next lesson we will further explore Kode Sherpa features.



Top comments (0)