diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a494ba1..bc33bf0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,6 @@ name: Smart contracts -on: - push: - branches: [main] - pull_request: - branches: [main] +on: ["push", "pull_request"] jobs: build: diff --git a/contracts/OffsetHelper.sol b/contracts/OffsetHelper.sol index 3bde11b..0b5d291 100644 --- a/contracts/OffsetHelper.sol +++ b/contracts/OffsetHelper.sol @@ -488,11 +488,10 @@ contract OffsetHelper is OffsetHelperStorage { balances[msg.sender][_tco2s[i]] >= _amounts[i], "Insufficient TCO2 balance" ); - - balances[msg.sender][_tco2s[i]] -= _amounts[i]; - - IToucanCarbonOffsets(_tco2s[i]).retire(_amounts[i]); - + if (_amounts[i] > 0) { + balances[msg.sender][_tco2s[i]] -= _amounts[i]; + IToucanCarbonOffsets(_tco2s[i]).retire(_amounts[i]); + } unchecked { ++i; } diff --git a/test/OffsetHelper.ts b/test/OffsetHelper.ts index d99bec9..7730d13 100644 --- a/test/OffsetHelper.ts +++ b/test/OffsetHelper.ts @@ -11,6 +11,7 @@ import { import * as hardhatContracts from "../utils/toucanContracts.json"; import * as poolContract from "../artifacts/contracts/interfaces/IToucanPoolToken.sol/IToucanPoolToken.json"; +import * as carbonOffsetsContract from "../artifacts/contracts/interfaces/IToucanCarbonOffsets.sol/IToucanCarbonOffsets.json"; import { IToucanPoolToken, OffsetHelper, @@ -18,7 +19,7 @@ import { Swapper, Swapper__factory, } from "../typechain"; -import addresses from "../utils/addresses"; +import addresses, { whaleAddresses } from "../utils/addresses"; import { Contract } from "ethers"; import { usdcABI, wethABI, wmaticABI } from "../utils/ABIs"; @@ -85,13 +86,31 @@ describe("Offset Helper - autoOffset", function () { ] ); - await Promise.all( - addrs.map(async (addr) => { - await addr.sendTransaction({ - to: addr2.address, - value: (await addr.getBalance()).sub(parseEther("1.0")), - }); - }) + // Transfer a large amount of MATIC and NCT to the test account via account impersonation. + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [whaleAddresses.matic], + }); + const maticWhale = ethers.provider.getSigner(whaleAddresses.matic); + await maticWhale.sendTransaction({ + to: addr2.address, + value: (await maticWhale.getBalance()).sub(parseEther("1.0")), + }); + + // Note: The swapper fails when trying to exchange such a large amount of NCT. + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [whaleAddresses.nct], + }); + const addrNctWhale = ethers.provider.getSigner(whaleAddresses.nct); + const nctWhaleSigner = new ethers.Contract( + addresses.nct, + hardhatContracts.contracts.NatureCarbonTonne.abi, + addrNctWhale + ); + await nctWhaleSigner.transfer( + addr2.address, + await nctWhaleSigner.balanceOf(whaleAddresses.nct) ); await swapper.swap(addresses.weth, parseEther("20.0"), { @@ -298,7 +317,7 @@ describe("Offset Helper - autoOffset", function () { ).to.be.revertedWith("Insufficient TCO2 balance"); }); - it("Should retire using an NCT deposit", async function () { + it("Should retire using a BCT deposit", async function () { await (await bct.approve(offsetHelper.address, parseEther("1.0"))).wait(); await ( @@ -319,6 +338,54 @@ describe("Offset Helper - autoOffset", function () { await expect(offsetHelper.autoRetire(tco2s, amounts)).to.not.be.reverted; }); + + it("Should retire using an NCT deposit, even if the first scored TCO2 is not in pool", async function () { + const scoredTCO2s = await nct.getScoredTCO2s(); + const lowestScoredTCO2 = new ethers.Contract( + scoredTCO2s[0], + carbonOffsetsContract.abi, + addr2 + ); + const lowestScoredTCO2Balance = await lowestScoredTCO2.balanceOf( + addresses.nct + ); + + // Skip setup if the oldest tco2's balance in the pool is already 0. + if (formatEther(lowestScoredTCO2Balance) !== "0.0") { + // Setup: If the oldest tco2 balance is non-zero, remove all its tokens from the pool via a redeem. + // Ensure that addr2 has enough NCT to redeem all of the lowestScoredTCO2 or setup will fail. + expect(await nct.balanceOf(addr2.address)).to.be.above( + await lowestScoredTCO2.balanceOf(addresses.nct) + ); + + await nct.approve(offsetHelper.address, lowestScoredTCO2Balance); + + await offsetHelper.deposit(addresses.nct, lowestScoredTCO2Balance); + + await offsetHelper.autoRedeem(addresses.nct, lowestScoredTCO2Balance); + } + + // Ensure the test condition is met. + expect(await lowestScoredTCO2.balanceOf(addresses.nct)).to.equal(0); + + await nct.approve(offsetHelper.address, parseEther("1.0")); + + await offsetHelper.deposit(addresses.nct, parseEther("0.0005")); + + const redeemReceipt = await ( + await offsetHelper.autoRedeem(addresses.nct, parseEther("0.0005")) + ).wait(); + + if (!redeemReceipt.events) { + return; + } + const tco2s = + redeemReceipt.events[redeemReceipt.events.length - 1].args?.tco2s; + const amounts = + redeemReceipt.events[redeemReceipt.events.length - 1].args?.amounts; + + await expect(offsetHelper.autoRetire(tco2s, amounts)).to.not.be.reverted; + }); }); describe("Testing deposit() and withdraw()", function () { diff --git a/utils/addresses.ts b/utils/addresses.ts index d3ed614..d88becf 100644 --- a/utils/addresses.ts +++ b/utils/addresses.ts @@ -7,7 +7,11 @@ interface IfcAddresses { wmatic: string; } -const addresses: IfcAddresses = { +interface IfcWhaleAddresses { + matic: string; + nct: string; +} +export const addresses: IfcAddresses = { myAddress: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", bct: "0x2F800Db0fdb5223b3C3f354886d907A671414A7F", nct: "0xD838290e877E0188a4A44700463419ED96c16107", @@ -16,6 +20,11 @@ const addresses: IfcAddresses = { wmatic: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", }; +export const whaleAddresses: IfcWhaleAddresses = { + matic: "0xe7804c37c13166fF0b37F5aE0BB07A3aEbb6e245", + nct: "0x4b3ebae392e8b90a9b13068e90b27d9c41abc3c8", +}; + export const mumbaiAddresses: IfcAddresses = { myAddress: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", bct: "0xf2438A14f668b1bbA53408346288f3d7C71c10a1",