¿What if you could earn passive income on your DAO treasury? ¿Or during your your token presale, auction or in any project where you have idle ERC20 tokens sitting on a contract? On this guide we'll launch a staking contract that lends on AAVE on the background.
We'll launch this contract on Scroll Sepolia but I'll show what needs to be done in order to deploy it on any other chain where AAVE is deployed.
Note: We'll use Remix and Blockscout for deploying and interacting with the contracts. Alternatively you can use this repo that includes commented code, foundry unit tests, and a github action.
Step1: Launch the contract
Make sure to have a wallet such as Metamask, connect it to Scroll Sepolia, and get funds from a faucet.
Launch the following contract on Remix by passing the aavePoolAddress
and stakedTokenAddress
addresses as constructor params. Use the following addresses deployed on Scroll Sepolia:
-
aavePoolAddress
:0x48914C788295b5db23aF2b5F0B3BE775C4eA9440
-
stakedTokenAddress
:0x7984E363c38b590bB4CA35aEd5133Ef2c6619C40
which is the AAVE testnet DAI on Scroll Sepolia
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
library DataTypes {
struct ReserveConfigurationMap {
uint256 data;
}
struct ReserveData {
ReserveConfigurationMap configuration;
uint128 liquidityIndex;
uint128 currentLiquidityRate;
uint128 variableBorrowIndex;
uint128 currentVariableBorrowRate;
uint128 currentStableBorrowRate;
uint40 lastUpdateTimestamp;
uint16 id;
address aTokenAddress;
address stableDebtTokenAddress;
address variableDebtTokenAddress;
address interestRateStrategyAddress;
uint128 accruedToTreasury;
uint128 unbacked;
uint128 isolationModeTotalDebt;
}
}
interface IPool {
function borrow(
address asset,
uint256 amount,
uint256 interestRateMode, // 1 for Stable, 2 for Variable
uint16 referralCode,
address onBehalfOf) external;
function supply(
address asset,
uint256 amount,
address onBehalfOf,
uint16 referralCode) external;
function withdraw(
address asset,
uint256 amount,
address to) external returns (uint256);
function getReserveData(
address asset) external view returns (DataTypes.ReserveData memory);
}
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
contract AaveLender {
address public immutable AAVE_POOL_ADDRESS;
address public immutable STAKED_TOKEN_ADDRESS;
address public immutable ATOKEN_ADDRESS;
address public immutable OWNER;
mapping(address account => uint amount) public stakeByAccount;
uint public totalStake;
constructor(address aavePoolAddress, address stakedTokenAddress) {
AAVE_POOL_ADDRESS = aavePoolAddress;
STAKED_TOKEN_ADDRESS = stakedTokenAddress;
OWNER = msg.sender;
ATOKEN_ADDRESS = IPool(aavePoolAddress).getReserveData(stakedTokenAddress).aTokenAddress;
}
function stake(uint amount) public {
totalStake += amount;
stakeByAccount[msg.sender] += amount;
IERC20(STAKED_TOKEN_ADDRESS).transferFrom(msg.sender, address(this), amount);
IERC20(STAKED_TOKEN_ADDRESS).approve(AAVE_POOL_ADDRESS, amount);
IPool(AAVE_POOL_ADDRESS).supply(
STAKED_TOKEN_ADDRESS,
amount,
address(this),
0);
}
function unstake(uint amount) public {
require(amount <= stakeByAccount[msg.sender], "Not enough stake");
totalStake -= amount;
stakeByAccount[msg.sender] -= amount;
IPool(AAVE_POOL_ADDRESS).withdraw(
STAKED_TOKEN_ADDRESS,
amount,
msg.sender
);
}
function yieldEarned() public view returns(uint){
return IERC20(ATOKEN_ADDRESS).balanceOf(address(this)) - totalStake;
}
function withdraw(uint amount) public {
require(msg.sender == OWNER, "Sender is not owner");
require(amount <= yieldEarned(), "Maximum withdraw exceeded");
IPool(AAVE_POOL_ADDRESS).withdraw(
STAKED_TOKEN_ADDRESS,
amount,
msg.sender
);
}
}
Step 2: Mint testnet DAI
Now go to the Aave Faucet, connect your wallet on Scroll Sepolia, click the Faucet button and confirm the transaction.
Once the transaction is confirmed you will be able to see your Testnet DAIs by adding the token by using the following address 0x7984E363c38b590bB4CA35aEd5133Ef2c6619C40
.
Remember that you're only able to borrow and lend tokens previously added to the Aave protocol. Also notice that this DAI version is not the same as other protocols on Sepolia and Scroll Sepolia such as Uniswap v3. This is a special version deployed by Aave and it's the only one compatible with Aave on Scroll Testnet. If you're on any mainnet chain you will be able to use the token official version.
Step3: Approve the Staking contract
We need to approve the Staking contract so it can access our funds.
We'll also do this on Scroll-Etherscan.
On the DAI contract at 0x7984E363c38b590bB4CA35aEd5133Ef2c6619C40
, pass the following params to the approve token function.
-
spender
: The staking contract you just launched -
amount
:100000000000000000000
which is 100 in wei format.
Step 4: Earn yield
Now you can stake by calling the stake
function and passing as parameter the amount of DAI you approved.
On every block, the yield will be increasing bit by bit and can be displaying by calling yieldEarned
. The yield can be sent to the owner by calling the withdraw
method.
Next steps
This demo shows how to lend on AAVE from a smart contract with minimal code. Now is up to you to use it to improve an existing project or to create something new. Personally, I would like to add this a lottery contract I'm working on so all the accumulated ticket sales generate yield while we wait for the lottery winner.. Let me know in the comments if you have ideas or questions about this tutorial.
Learn more about Aave on the official documentation.
Thanks for watching this guide!
Follow Filosofía Código on dev.to and in Youtube for everything related to Blockchain development.