diff --git a/e2e/generateDeployments.js b/e2e/generateDeployments.js index d19a4e8a8..19655835e 100755 --- a/e2e/generateDeployments.js +++ b/e2e/generateDeployments.js @@ -100,11 +100,20 @@ async function run() { perpsFactory.contracts.PerpsAccountProxy ?? perpsFactory.contracts.AccountProxy; } - const rd = + const snxRewards = deployments?.state?.[`provision.spartan_council_pool_rewards`]?.artifacts?.imports ?.spartan_council_pool_rewards; - if (rd) { - contracts[`RewardsDistributorForSpartanCouncilPool`] = rd.contracts.RewardsDistributor; + if (snxRewards) { + contracts[`RewardsDistributorForSpartanCouncilPoolSNX`] = + snxRewards.contracts.RewardsDistributor; + } + + const usdcRewards = + deployments?.state?.[`provision.sccp_313_spartan_council_pool_usdc_rewards`]?.artifacts?.imports + ?.sccp_313_spartan_council_pool_usdc_rewards; + if (usdcRewards) { + contracts[`RewardsDistributorForSpartanCouncilPoolUSDC`] = + usdcRewards.contracts.RewardsDistributor; } function mintableToken(provisionStep) { diff --git a/e2e/tasks/getTokenRewardsDistributorRewardsAmount.js b/e2e/tasks/getTokenRewardsDistributorRewardsAmount.js index 68ed1233e..aa1c01088 100755 --- a/e2e/tasks/getTokenRewardsDistributorRewardsAmount.js +++ b/e2e/tasks/getTokenRewardsDistributorRewardsAmount.js @@ -10,13 +10,25 @@ async function getTokenRewardsDistributorRewardsAmount({ distributorAddress }) { ); const RewardsDistributor = new ethers.Contract( distributorAddress, - ['function rewardsAmount() view returns (uint256)'], + [ + 'function payoutToken() view returns (address)', + 'function rewardsAmount() view returns (uint256)', + ], provider ); - const rewardsAmount = await RewardsDistributor.rewardsAmount().catch(() => null); - log({ rewardsAmount }); + const [payoutToken, rewardsAmount] = await Promise.all([ + RewardsDistributor.payoutToken().catch(() => null), + RewardsDistributor.rewardsAmount().catch(() => null), + ]); - return parseFloat(ethers.utils.formatEther(rewardsAmount)); + const Token = new ethers.Contract( + payoutToken, + ['function decimals() view returns (uint8)'], + provider + ); + const decimals = await Token.decimals().catch(() => null); + + return parseFloat(ethers.utils.formatUnits(rewardsAmount, decimals)); } module.exports = { diff --git a/e2e/tasks/setUSDCTokenBalance.js b/e2e/tasks/setUSDCTokenBalance.js index ace8e5ed7..dcb133915 100755 --- a/e2e/tasks/setUSDCTokenBalance.js +++ b/e2e/tasks/setUSDCTokenBalance.js @@ -33,7 +33,7 @@ async function setUSDCTokenBalance({ wallet, balance }) { } // Mainnet only! - const friendlyWhale = '0x20FE51A9229EEf2cF8Ad9E89d91CAb9312cF3b7A'; + const friendlyWhale = '0xcdac0d6c6c59727a65f871236188350531885c43'; const whaleBalance = parseFloat( ethers.utils.formatUnits(await Token.balanceOf(friendlyWhale), decimals) ); diff --git a/e2e/tests/omnibus-base-mainnet-andromeda.toml/Rewards_USDC.e2e.js b/e2e/tests/omnibus-base-mainnet-andromeda.toml/Rewards_USDC.e2e.js new file mode 100644 index 000000000..8bfbecf14 --- /dev/null +++ b/e2e/tests/omnibus-base-mainnet-andromeda.toml/Rewards_USDC.e2e.js @@ -0,0 +1,332 @@ +const crypto = require('crypto'); +const assert = require('assert'); +const { ethers } = require('ethers'); +require('../../inspect'); +const log = require('debug')(`e2e:${require('path').basename(__filename, '.e2e.js')}`); + +const { getEthBalance } = require('../../tasks/getEthBalance'); +const { setEthBalance } = require('../../tasks/setEthBalance'); +const { setUSDCTokenBalance } = require('../../tasks/setUSDCTokenBalance'); +const { wrapCollateral } = require('../../tasks/wrapCollateral'); +const { getAccountOwner } = require('../../tasks/getAccountOwner'); +const { createAccount } = require('../../tasks/createAccount'); +const { getCollateralBalance } = require('../../tasks/getCollateralBalance'); +const { getAccountCollateral } = require('../../tasks/getAccountCollateral'); +const { isCollateralApproved } = require('../../tasks/isCollateralApproved'); +const { approveCollateral } = require('../../tasks/approveCollateral'); +const { depositCollateral } = require('../../tasks/depositCollateral'); +const { delegateCollateral } = require('../../tasks/delegateCollateral'); +const { doPriceUpdate } = require('../../tasks/doPriceUpdate'); +const { syncTime } = require('../../tasks/syncTime'); +const { getTokenBalance } = require('../../tasks/getTokenBalance'); +const { transferToken } = require('../../tasks/transferToken'); +const { distributeRewards } = require('../../tasks/distributeRewards'); +const { getPoolOwner } = require('../../tasks/getPoolOwner'); +const { getTokenRewardsDistributorInfo } = require('../../tasks/getTokenRewardsDistributorInfo'); +const { + getTokenRewardsDistributorRewardsAmount, +} = require('../../tasks/getTokenRewardsDistributorRewardsAmount'); +const { getAvailableRewards } = require('../../tasks/getAvailableRewards'); +const { claimRewards } = require('../../tasks/claimRewards'); + +const { + contracts: { + RewardsDistributorForSpartanCouncilPoolUSDC: distributorAddress, + USDCToken: payoutToken, + + CoreProxy: rewardManager, + SynthUSDCToken: collateralType, + }, +} = require('../../deployments/meta.json'); + +describe(require('path').basename(__filename, '.e2e.js'), function () { + const accountId = parseInt(`1337${crypto.randomInt(1000)}`); + const provider = new ethers.providers.JsonRpcProvider( + process.env.RPC_URL || 'http://127.0.0.1:8545' + ); + const wallet = ethers.Wallet.createRandom().connect(provider); + const address = wallet.address; + const privateKey = wallet.privateKey; + + let snapshot; + let initialBalance; + let initialRewardsAmount; + + before('Create snapshot', async () => { + snapshot = await provider.send('evm_snapshot', []); + log('Create snapshot', { snapshot }); + + initialBalance = await getTokenBalance({ + walletAddress: distributorAddress, + tokenAddress: payoutToken, + }); + log('Initial balance', { initialBalance }); + + initialRewardsAmount = await getTokenRewardsDistributorRewardsAmount({ distributorAddress }); + log('Initial rewards amount', { initialRewardsAmount }); + }); + + after('Restore snapshot', async () => { + log('Restore snapshot', { snapshot }); + await provider.send('evm_revert', [snapshot]); + }); + + it('should sync time of the fork', async () => { + await syncTime(); + }); + + it('should validate Rewards Distributor info', async () => { + const info = await getTokenRewardsDistributorInfo({ distributorAddress }); + assert.equal(info.name, 'Spartan Council Pool USDC Rewards', 'name'); + assert.equal(info.poolId, 1, 'poolId'); + assert.equal(info.collateralType, collateralType, 'collateralType'); + assert.equal( + `${info.payoutToken}`.toLowerCase(), + `${payoutToken}`.toLowerCase(), + 'payoutToken' + ); + assert.equal(info.precision, 10 ** 6, 'precision'); + assert.equal(`${info.token}`.toLowerCase(), `${payoutToken}`.toLowerCase(), 'token'); + assert.equal(info.rewardManager, rewardManager, 'rewardManager'); + assert.equal(info.shouldFailPayout, false, 'shouldFailPayout'); + }); + + it('should create new random wallet', async () => { + log({ wallet: wallet.address, pk: wallet.privateKey }); + assert.ok(wallet.address); + }); + + it('should set ETH balance to 100', async () => { + assert.equal(await getEthBalance({ address }), 0, 'New wallet has 0 ETH balance'); + await setEthBalance({ address, balance: 100 }); + assert.equal(await getEthBalance({ address }), 100); + }); + + it('should create user account', async () => { + assert.equal( + await getAccountOwner({ accountId }), + ethers.constants.AddressZero, + 'New wallet should not have an account yet' + ); + await createAccount({ wallet, accountId }); + assert.equal(await getAccountOwner({ accountId }), address); + }); + + it(`should set USDC balance to 1_000`, async () => { + assert.equal( + await getCollateralBalance({ address, symbol: 'USDC' }), + 0, + 'New wallet has 0 USDC balance' + ); + await setUSDCTokenBalance({ + wallet, + balance: 1_000, + }); + assert.equal(await getCollateralBalance({ address, symbol: 'USDC' }), 1_000); + }); + + it('should approve USDC spending for SpotMarket', async () => { + assert.equal( + await isCollateralApproved({ + address, + symbol: 'USDC', + spenderAddress: require('../../deployments/SpotMarketProxy.json').address, + }), + false, + 'New wallet has not allowed SpotMarket USDC spending' + ); + await approveCollateral({ + privateKey, + symbol: 'USDC', + spenderAddress: require('../../deployments/SpotMarketProxy.json').address, + }); + assert.equal( + await isCollateralApproved({ + address, + symbol: 'USDC', + spenderAddress: require('../../deployments/SpotMarketProxy.json').address, + }), + true + ); + }); + + it(`should wrap 1_000 USDC`, async () => { + const balance = await wrapCollateral({ wallet, symbol: 'USDC', amount: 1_000 }); + assert.equal(balance, 1_000); + }); + + it('should approve sUSDC spending for CoreProxy', async () => { + assert.equal( + await isCollateralApproved({ + address, + symbol: 'sUSDC', + spenderAddress: require('../../deployments/CoreProxy.json').address, + }), + false, + 'New wallet has not allowed CoreProxy sUSDC spending' + ); + await approveCollateral({ + privateKey, + symbol: 'sUSDC', + spenderAddress: require('../../deployments/CoreProxy.json').address, + }); + assert.equal( + await isCollateralApproved({ + address, + symbol: 'sUSDC', + spenderAddress: require('../../deployments/CoreProxy.json').address, + }), + true + ); + }); + + it(`should deposit 1_000 sUSDC into the system`, async () => { + assert.equal(await getCollateralBalance({ address, symbol: 'sUSDC' }), 1_000); + assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { + totalDeposited: 0, + totalAssigned: 0, + totalLocked: 0, + }); + + await depositCollateral({ + privateKey, + symbol: 'sUSDC', + accountId, + amount: 1_000, + }); + + assert.equal(await getCollateralBalance({ address, symbol: 'sUSDC' }), 0); + assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { + totalDeposited: 1_000, + totalAssigned: 0, + totalLocked: 0, + }); + }); + + it('should make a price update', async () => { + // We must sync timestamp of the fork before making price updates + await syncTime(); + + // delegating collateral and views requiring price will fail if there's no price update within the last hour, + // so we send off a price update just to be safe + await doPriceUpdate({ + wallet, + marketId: 100, + settlementStrategyId: require('../../deployments/extras.json').eth_pyth_settlement_strategy, + }); + await doPriceUpdate({ + wallet, + marketId: 200, + settlementStrategyId: require('../../deployments/extras.json').btc_pyth_settlement_strategy, + }); + }); + + it(`should delegate 1_000 sUSDC into the Spartan Council pool`, async () => { + assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { + totalDeposited: 1_000, + totalAssigned: 0, + totalLocked: 0, + }); + await delegateCollateral({ + privateKey, + symbol: 'sUSDC', + accountId, + amount: 1_000, + poolId: 1, + }); + assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { + totalDeposited: 1_000, + totalAssigned: 1_000, + totalLocked: 0, + }); + }); + + it('should fund RewardDistributor with 1_000 USDC', async () => { + await setUSDCTokenBalance({ wallet, balance: 1_000 }); + + await transferToken({ + privateKey, + tokenAddress: payoutToken, + targetWalletAddress: distributorAddress, + amount: 1_000, + }); + + assert.equal( + Math.floor( + await getTokenBalance({ walletAddress: distributorAddress, tokenAddress: payoutToken }) + ), + Math.floor(initialBalance + 1_000), + 'Rewards Distributor has 1_000 extra USDC on its balance' + ); + }); + + it('should distribute 1_000 USDC rewards', async () => { + const poolId = 1; + const poolOwner = await getPoolOwner({ poolId }); + log({ poolOwner }); + + await provider.send('anvil_impersonateAccount', [poolOwner]); + const signer = provider.getSigner(poolOwner); + + const amount = ethers.utils.parseUnits(`${1_000}`, 6); // the number must be in 6 decimals + const start = Math.floor(Date.now() / 1_000); + const duration = 10; + + await distributeRewards({ + wallet: signer, + distributorAddress, + poolId, + collateralType, + amount, + start, + duration, + }); + + await provider.send('anvil_stopImpersonatingAccount', [poolOwner]); + + assert.equal( + await getTokenRewardsDistributorRewardsAmount({ distributorAddress }), + initialRewardsAmount + 1_000, + 'should have 1_000 extra tokens in rewards' + ); + }); + + it('should claim USDC rewards', async () => { + const poolId = 1; + + const availableRewards = await getAvailableRewards({ + accountId, + poolId, + collateralType, + distributorAddress, + }); + + assert.ok(availableRewards > 0, 'should have some rewards to claim'); + + assert.equal( + await getTokenBalance({ walletAddress: address, tokenAddress: payoutToken }), + 0, + 'Wallet has 0 USDC balance BEFORE claim' + ); + + await claimRewards({ + wallet, + accountId, + poolId, + collateralType, + distributorAddress, + }); + + const postClaimBalance = await getTokenBalance({ + walletAddress: address, + tokenAddress: payoutToken, + }); + assert.ok(postClaimBalance > 0, 'Wallet has some non-zero USDC balance AFTER claim'); + + assert.equal( + Math.floor(await getTokenRewardsDistributorRewardsAmount({ distributorAddress })), + Math.floor(initialRewardsAmount + 1_000 - postClaimBalance), + 'should deduct claimed token amount from total distributor rewards amount' + ); + }); +}); diff --git a/e2e/tests/omnibus-base-sepolia-andromeda.toml/Rewards.e2e.js b/e2e/tests/omnibus-base-sepolia-andromeda.toml/Rewards_SNX.e2e.js similarity index 76% rename from e2e/tests/omnibus-base-sepolia-andromeda.toml/Rewards.e2e.js rename to e2e/tests/omnibus-base-sepolia-andromeda.toml/Rewards_SNX.e2e.js index 4d863140b..18e7be6ef 100644 --- a/e2e/tests/omnibus-base-sepolia-andromeda.toml/Rewards.e2e.js +++ b/e2e/tests/omnibus-base-sepolia-andromeda.toml/Rewards_SNX.e2e.js @@ -18,10 +18,6 @@ const { approveCollateral } = require('../../tasks/approveCollateral'); const { depositCollateral } = require('../../tasks/depositCollateral'); const { delegateCollateral } = require('../../tasks/delegateCollateral'); const { doPriceUpdate } = require('../../tasks/doPriceUpdate'); -const { setSpotWrapper } = require('../../tasks/setSpotWrapper'); -const { - configureMaximumMarketCollateral, -} = require('../../tasks/configureMaximumMarketCollateral'); const { syncTime } = require('../../tasks/syncTime'); const { getTokenBalance } = require('../../tasks/getTokenBalance'); const { transferToken } = require('../../tasks/transferToken'); @@ -37,15 +33,13 @@ const { claimRewards } = require('../../tasks/claimRewards'); const { contracts: { - RewardsDistributorForSpartanCouncilPool: distributorAddress, + RewardsDistributorForSpartanCouncilPoolSNX: distributorAddress, FakeCollateralfwSNX: payoutToken, CoreProxy: rewardManager, SynthUSDCToken: collateralType, }, } = require('../../deployments/meta.json'); -const SYNTH_USDC_MAX_MARKET_COLLATERAL = 10_000_000; - describe(require('path').basename(__filename, '.e2e.js'), function () { const accountId = parseInt(`1337${crypto.randomInt(1000)}`); const provider = new ethers.providers.JsonRpcProvider( @@ -57,6 +51,7 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { let snapshot; let initialBalance; + let initialRewardsAmount; before('Create snapshot', async () => { snapshot = await provider.send('evm_snapshot', []); @@ -67,6 +62,9 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { tokenAddress: payoutToken, }); log('Initial balance', { initialBalance }); + + initialRewardsAmount = await getTokenRewardsDistributorRewardsAmount({ distributorAddress }); + log('Initial rewards amount', { initialRewardsAmount }); }); after('Restore snapshot', async () => { @@ -78,6 +76,22 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { await syncTime(); }); + it('should validate Rewards Distributor info', async () => { + const info = await getTokenRewardsDistributorInfo({ distributorAddress }); + assert.equal(info.name, 'Spartan Council Pool Rewards', 'name'); + assert.equal(info.poolId, 1, 'poolId'); + assert.equal(info.collateralType, collateralType, 'collateralType'); + assert.equal( + `${info.payoutToken}`.toLowerCase(), + `${payoutToken}`.toLowerCase(), + 'payoutToken' + ); + assert.equal(info.precision, 10 ** 18, 'precision'); + assert.equal(`${info.token}`.toLowerCase(), `${payoutToken}`.toLowerCase(), 'token'); + assert.equal(info.rewardManager, rewardManager, 'rewardManager'); + assert.equal(info.shouldFailPayout, false, 'shouldFailPayout'); + }); + it('should create new random wallet', async () => { log({ wallet: wallet.address, pk: wallet.privateKey }); assert.ok(wallet.address); @@ -99,7 +113,7 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { assert.equal(await getAccountOwner({ accountId }), address); }); - it(`should set fUSDC balance to ${SYNTH_USDC_MAX_MARKET_COLLATERAL * 2}`, async () => { + it(`should set fUSDC balance to 1_000`, async () => { const { tokenAddress } = await getCollateralConfig('fUSDC'); assert.equal( await getCollateralBalance({ address, symbol: 'fUSDC' }), @@ -109,12 +123,9 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { await setMintableTokenBalance({ privateKey, tokenAddress, - balance: SYNTH_USDC_MAX_MARKET_COLLATERAL * 2, + balance: 1_000, }); - assert.equal( - await getCollateralBalance({ address, symbol: 'fUSDC' }), - SYNTH_USDC_MAX_MARKET_COLLATERAL * 2 - ); + assert.equal(await getCollateralBalance({ address, symbol: 'fUSDC' }), 1_000); }); it('should approve fUSDC spending for SpotMarket', async () => { @@ -142,26 +153,9 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { ); }); - it(`should increase max collateral for the test to ${SYNTH_USDC_MAX_MARKET_COLLATERAL * 2}`, async () => { - await configureMaximumMarketCollateral({ - marketId: require('../../deployments/extras.json').synth_usdc_market_id, - symbol: 'fUSDC', - targetAmount: String(SYNTH_USDC_MAX_MARKET_COLLATERAL * 2), - }); - await setSpotWrapper({ - marketId: require('../../deployments/extras.json').synth_usdc_market_id, - symbol: 'fUSDC', - targetAmount: String(SYNTH_USDC_MAX_MARKET_COLLATERAL * 2), - }); - }); - - it(`should wrap ${SYNTH_USDC_MAX_MARKET_COLLATERAL} fUSDC`, async () => { - const balance = await wrapCollateral({ - wallet, - symbol: 'fUSDC', - amount: SYNTH_USDC_MAX_MARKET_COLLATERAL, - }); - assert.equal(balance, SYNTH_USDC_MAX_MARKET_COLLATERAL); + it(`should wrap 1_000 fUSDC`, async () => { + const balance = await wrapCollateral({ wallet, symbol: 'fUSDC', amount: 1_000 }); + assert.equal(balance, 1_000); }); it('should approve sUSDC spending for CoreProxy', async () => { @@ -189,11 +183,8 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { ); }); - it(`should deposit ${SYNTH_USDC_MAX_MARKET_COLLATERAL - 100_000} sUSDC into the system`, async () => { - assert.equal( - await getCollateralBalance({ address, symbol: 'sUSDC' }), - SYNTH_USDC_MAX_MARKET_COLLATERAL - ); + it(`should deposit 1_000 sUSDC into the system`, async () => { + assert.equal(await getCollateralBalance({ address, symbol: 'sUSDC' }), 1_000); assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { totalDeposited: 0, totalAssigned: 0, @@ -204,12 +195,12 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { privateKey, symbol: 'sUSDC', accountId, - amount: SYNTH_USDC_MAX_MARKET_COLLATERAL - 100_000, + amount: 1_000, }); - assert.equal(await getCollateralBalance({ address, symbol: 'sUSDC' }), 100_000); + assert.equal(await getCollateralBalance({ address, symbol: 'sUSDC' }), 0); assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { - totalDeposited: SYNTH_USDC_MAX_MARKET_COLLATERAL - 100_000, + totalDeposited: 1_000, totalAssigned: 0, totalLocked: 0, }); @@ -233,9 +224,9 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { }); }); - it(`should delegate ${SYNTH_USDC_MAX_MARKET_COLLATERAL - 200_000} sUSDC into the Spartan Council pool`, async () => { + it(`should delegate 1_000 sUSDC into the Spartan Council pool`, async () => { assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { - totalDeposited: SYNTH_USDC_MAX_MARKET_COLLATERAL - 100_000, + totalDeposited: 1_000, totalAssigned: 0, totalLocked: 0, }); @@ -243,54 +234,36 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { privateKey, symbol: 'sUSDC', accountId, - amount: SYNTH_USDC_MAX_MARKET_COLLATERAL - 200_000, + amount: 1_000, poolId: 1, }); assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { - totalDeposited: SYNTH_USDC_MAX_MARKET_COLLATERAL - 100_000, - totalAssigned: SYNTH_USDC_MAX_MARKET_COLLATERAL - 200_000, + totalDeposited: 1_000, + totalAssigned: 1_000, totalLocked: 0, }); }); - it('should validate Rewards Distributor info', async () => { - const info = await getTokenRewardsDistributorInfo({ distributorAddress }); - assert.equal(info.name, 'Spartan Council Pool Rewards', 'name'); - assert.equal(info.poolId, 1, 'poolId'); - assert.equal(info.collateralType, collateralType, 'collateralType'); - assert.equal(info.payoutToken, payoutToken, 'payoutToken'); - assert.equal(info.precision, 10 ** 18, 'precision'); - assert.equal(info.token, payoutToken, 'token'); - assert.equal(info.rewardManager, rewardManager, 'rewardManager'); - assert.equal(info.shouldFailPayout, false, 'shouldFailPayout'); - }); - - it('should fund RewardDistributor with 1_000_000 fwSNX', async () => { - await setPermissionlessTokenBalance({ - privateKey, - tokenAddress: payoutToken, - balance: 1_000_000, - }); + it('should fund RewardDistributor with 1_000 fwSNX', async () => { + await setPermissionlessTokenBalance({ privateKey, tokenAddress: payoutToken, balance: 1_000 }); await transferToken({ privateKey, tokenAddress: payoutToken, targetWalletAddress: distributorAddress, - amount: 1_000_000, + amount: 1_000, }); assert.equal( Math.floor( await getTokenBalance({ walletAddress: distributorAddress, tokenAddress: payoutToken }) ), - Math.floor(initialBalance + 1_000_000), - 'Rewards Distributor has 1_000_000 fwSNX balance' + Math.floor(initialBalance + 1_000), + 'Rewards Distributor has 1_000 extra fwSNX on its balance' ); }); - it('should distribute 1_000_000 fwSNX rewards', async () => { - assert.equal(await getTokenRewardsDistributorRewardsAmount({ distributorAddress }), 0); - + it('should distribute 1_000 fwSNX rewards', async () => { const poolId = 1; const poolOwner = await getPoolOwner({ poolId }); log({ poolOwner }); @@ -298,8 +271,8 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { await provider.send('anvil_impersonateAccount', [poolOwner]); const signer = provider.getSigner(poolOwner); - const amount = ethers.utils.parseEther(`${1_000_000}`); - const start = Math.floor(Date.now() / 1000); + const amount = ethers.utils.parseUnits(`${1_000}`, 18); // the number must be in 18 decimals + const start = Math.floor(Date.now() / 1_000); const duration = 10; await distributeRewards({ @@ -314,7 +287,11 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { await provider.send('anvil_stopImpersonatingAccount', [poolOwner]); - assert.equal(await getTokenRewardsDistributorRewardsAmount({ distributorAddress }), 1_000_000); + assert.equal( + await getTokenRewardsDistributorRewardsAmount({ distributorAddress }), + initialRewardsAmount + 1_000, + 'should have 1_000 extra tokens in rewards' + ); }); it('should claim fwSNX rewards', async () => { @@ -351,7 +328,7 @@ describe(require('path').basename(__filename, '.e2e.js'), function () { assert.equal( Math.floor(await getTokenRewardsDistributorRewardsAmount({ distributorAddress })), - Math.floor(1_000_000 - postClaimBalance), + Math.floor(initialRewardsAmount + 1_000 - postClaimBalance), 'should deduct claimed token amount from total distributor rewards amount' ); }); diff --git a/e2e/tests/omnibus-base-sepolia-andromeda.toml/Rewards_USDC.e2e.js b/e2e/tests/omnibus-base-sepolia-andromeda.toml/Rewards_USDC.e2e.js new file mode 100644 index 000000000..42786d47f --- /dev/null +++ b/e2e/tests/omnibus-base-sepolia-andromeda.toml/Rewards_USDC.e2e.js @@ -0,0 +1,335 @@ +const crypto = require('crypto'); +const assert = require('assert'); +const { ethers } = require('ethers'); +require('../../inspect'); +const log = require('debug')(`e2e:${require('path').basename(__filename, '.e2e.js')}`); + +const { getEthBalance } = require('../../tasks/getEthBalance'); +const { setEthBalance } = require('../../tasks/setEthBalance'); +const { setMintableTokenBalance } = require('../../tasks/setMintableTokenBalance'); +const { wrapCollateral } = require('../../tasks/wrapCollateral'); +const { getAccountOwner } = require('../../tasks/getAccountOwner'); +const { createAccount } = require('../../tasks/createAccount'); +const { getCollateralConfig } = require('../../tasks/getCollateralConfig'); +const { getCollateralBalance } = require('../../tasks/getCollateralBalance'); +const { getAccountCollateral } = require('../../tasks/getAccountCollateral'); +const { isCollateralApproved } = require('../../tasks/isCollateralApproved'); +const { approveCollateral } = require('../../tasks/approveCollateral'); +const { depositCollateral } = require('../../tasks/depositCollateral'); +const { delegateCollateral } = require('../../tasks/delegateCollateral'); +const { doPriceUpdate } = require('../../tasks/doPriceUpdate'); +const { syncTime } = require('../../tasks/syncTime'); +const { getTokenBalance } = require('../../tasks/getTokenBalance'); +const { transferToken } = require('../../tasks/transferToken'); +const { distributeRewards } = require('../../tasks/distributeRewards'); +const { getPoolOwner } = require('../../tasks/getPoolOwner'); +const { getTokenRewardsDistributorInfo } = require('../../tasks/getTokenRewardsDistributorInfo'); +const { + getTokenRewardsDistributorRewardsAmount, +} = require('../../tasks/getTokenRewardsDistributorRewardsAmount'); +const { getAvailableRewards } = require('../../tasks/getAvailableRewards'); +const { claimRewards } = require('../../tasks/claimRewards'); + +const { + contracts: { + RewardsDistributorForSpartanCouncilPoolUSDC: distributorAddress, + FakeCollateralfUSDC: payoutToken, + + CoreProxy: rewardManager, + SynthUSDCToken: collateralType, + }, +} = require('../../deployments/meta.json'); + +describe(require('path').basename(__filename, '.e2e.js'), function () { + const accountId = parseInt(`1337${crypto.randomInt(1000)}`); + const provider = new ethers.providers.JsonRpcProvider( + process.env.RPC_URL || 'http://127.0.0.1:8545' + ); + const wallet = ethers.Wallet.createRandom().connect(provider); + const address = wallet.address; + const privateKey = wallet.privateKey; + + let snapshot; + let initialBalance; + let initialRewardsAmount; + + before('Create snapshot', async () => { + snapshot = await provider.send('evm_snapshot', []); + log('Create snapshot', { snapshot }); + + initialBalance = await getTokenBalance({ + walletAddress: distributorAddress, + tokenAddress: payoutToken, + }); + log('Initial balance', { initialBalance }); + + initialRewardsAmount = await getTokenRewardsDistributorRewardsAmount({ distributorAddress }); + log('Initial rewards amount', { initialRewardsAmount }); + }); + + after('Restore snapshot', async () => { + log('Restore snapshot', { snapshot }); + await provider.send('evm_revert', [snapshot]); + }); + + it('should sync time of the fork', async () => { + await syncTime(); + }); + + it('should validate Rewards Distributor info', async () => { + const info = await getTokenRewardsDistributorInfo({ distributorAddress }); + assert.equal(info.name, 'Spartan Council Pool USDC Rewards', 'name'); + assert.equal(info.poolId, 1, 'poolId'); + assert.equal(info.collateralType, collateralType, 'collateralType'); + assert.equal( + `${info.payoutToken}`.toLowerCase(), + `${payoutToken}`.toLowerCase(), + 'payoutToken' + ); + assert.equal(info.precision, 10 ** 6, 'precision'); + assert.equal(`${info.token}`.toLowerCase(), `${payoutToken}`.toLowerCase(), 'token'); + assert.equal(info.rewardManager, rewardManager, 'rewardManager'); + assert.equal(info.shouldFailPayout, false, 'shouldFailPayout'); + }); + + it('should create new random wallet', async () => { + log({ wallet: wallet.address, pk: wallet.privateKey }); + assert.ok(wallet.address); + }); + + it('should set ETH balance to 100', async () => { + assert.equal(await getEthBalance({ address }), 0, 'New wallet has 0 ETH balance'); + await setEthBalance({ address, balance: 100 }); + assert.equal(await getEthBalance({ address }), 100); + }); + + it('should create user account', async () => { + assert.equal( + await getAccountOwner({ accountId }), + ethers.constants.AddressZero, + 'New wallet should not have an account yet' + ); + await createAccount({ wallet, accountId }); + assert.equal(await getAccountOwner({ accountId }), address); + }); + + it(`should set fUSDC balance to 1_000`, async () => { + const { tokenAddress } = await getCollateralConfig('fUSDC'); + assert.equal( + await getCollateralBalance({ address, symbol: 'fUSDC' }), + 0, + 'New wallet has 0 fUSDC balance' + ); + await setMintableTokenBalance({ + privateKey, + tokenAddress, + balance: 1_000, + }); + assert.equal(await getCollateralBalance({ address, symbol: 'fUSDC' }), 1_000); + }); + + it('should approve fUSDC spending for SpotMarket', async () => { + assert.equal( + await isCollateralApproved({ + address, + symbol: 'fUSDC', + spenderAddress: require('../../deployments/SpotMarketProxy.json').address, + }), + false, + 'New wallet has not allowed SpotMarket fUSDC spending' + ); + await approveCollateral({ + privateKey, + symbol: 'fUSDC', + spenderAddress: require('../../deployments/SpotMarketProxy.json').address, + }); + assert.equal( + await isCollateralApproved({ + address, + symbol: 'fUSDC', + spenderAddress: require('../../deployments/SpotMarketProxy.json').address, + }), + true + ); + }); + + it(`should wrap 1_000 fUSDC`, async () => { + const balance = await wrapCollateral({ wallet, symbol: 'fUSDC', amount: 1_000 }); + assert.equal(balance, 1_000); + }); + + it('should approve sUSDC spending for CoreProxy', async () => { + assert.equal( + await isCollateralApproved({ + address, + symbol: 'sUSDC', + spenderAddress: require('../../deployments/CoreProxy.json').address, + }), + false, + 'New wallet has not allowed CoreProxy sUSDC spending' + ); + await approveCollateral({ + privateKey, + symbol: 'sUSDC', + spenderAddress: require('../../deployments/CoreProxy.json').address, + }); + assert.equal( + await isCollateralApproved({ + address, + symbol: 'sUSDC', + spenderAddress: require('../../deployments/CoreProxy.json').address, + }), + true + ); + }); + + it(`should deposit 1_000 sUSDC into the system`, async () => { + assert.equal(await getCollateralBalance({ address, symbol: 'sUSDC' }), 1_000); + assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { + totalDeposited: 0, + totalAssigned: 0, + totalLocked: 0, + }); + + await depositCollateral({ + privateKey, + symbol: 'sUSDC', + accountId, + amount: 1_000, + }); + + assert.equal(await getCollateralBalance({ address, symbol: 'sUSDC' }), 0); + assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { + totalDeposited: 1_000, + totalAssigned: 0, + totalLocked: 0, + }); + }); + + it('should make a price update', async () => { + // We must sync timestamp of the fork before making price updates + await syncTime(); + + // delegating collateral and views requiring price will fail if there's no price update within the last hour, + // so we send off a price update just to be safe + await doPriceUpdate({ + wallet, + marketId: 100, + settlementStrategyId: require('../../deployments/extras.json').eth_pyth_settlement_strategy, + }); + await doPriceUpdate({ + wallet, + marketId: 200, + settlementStrategyId: require('../../deployments/extras.json').btc_pyth_settlement_strategy, + }); + }); + + it(`should delegate 1_000 sUSDC into the Spartan Council pool`, async () => { + assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { + totalDeposited: 1_000, + totalAssigned: 0, + totalLocked: 0, + }); + await delegateCollateral({ + privateKey, + symbol: 'sUSDC', + accountId, + amount: 1_000, + poolId: 1, + }); + assert.deepEqual(await getAccountCollateral({ accountId, symbol: 'sUSDC' }), { + totalDeposited: 1_000, + totalAssigned: 1_000, + totalLocked: 0, + }); + }); + + it('should fund RewardDistributor with 1_000 fUSDC', async () => { + await setMintableTokenBalance({ privateKey, tokenAddress: payoutToken, balance: 1_000 }); + + await transferToken({ + privateKey, + tokenAddress: payoutToken, + targetWalletAddress: distributorAddress, + amount: 1_000, + }); + + assert.equal( + Math.floor( + await getTokenBalance({ walletAddress: distributorAddress, tokenAddress: payoutToken }) + ), + Math.floor(initialBalance + 1_000), + 'Rewards Distributor has 1_000 extra fUSDC on its balance' + ); + }); + + it('should distribute 1_000 fUSDC rewards', async () => { + const poolId = 1; + const poolOwner = await getPoolOwner({ poolId }); + log({ poolOwner }); + + await provider.send('anvil_impersonateAccount', [poolOwner]); + const signer = provider.getSigner(poolOwner); + + const amount = ethers.utils.parseUnits(`${1_000}`, 6); // the number must be in 6 decimals + const start = Math.floor(Date.now() / 1_000); + const duration = 10; + + await distributeRewards({ + wallet: signer, + distributorAddress, + poolId, + collateralType, + amount, + start, + duration, + }); + + await provider.send('anvil_stopImpersonatingAccount', [poolOwner]); + + assert.equal( + await getTokenRewardsDistributorRewardsAmount({ distributorAddress }), + initialRewardsAmount + 1_000, + 'should have 1_000 extra tokens in rewards' + ); + }); + + it('should claim fUSDC rewards', async () => { + const poolId = 1; + + const availableRewards = await getAvailableRewards({ + accountId, + poolId, + collateralType, + distributorAddress, + }); + + assert.ok(availableRewards > 0, 'should have some rewards to claim'); + + assert.equal( + await getTokenBalance({ walletAddress: address, tokenAddress: payoutToken }), + 0, + 'Wallet has 0 fUSDC balance BEFORE claim' + ); + + await claimRewards({ + wallet, + accountId, + poolId, + collateralType, + distributorAddress, + }); + + const postClaimBalance = await getTokenBalance({ + walletAddress: address, + tokenAddress: payoutToken, + }); + assert.ok(postClaimBalance > 0, 'Wallet has some non-zero fUSDC balance AFTER claim'); + + assert.equal( + Math.floor(await getTokenRewardsDistributorRewardsAmount({ distributorAddress })), + Math.floor(initialRewardsAmount + 1_000 - postClaimBalance), + 'should deduct claimed token amount from total distributor rewards amount' + ); + }); +}); diff --git a/omnibus-base-mainnet-andromeda.toml b/omnibus-base-mainnet-andromeda.toml index 40545fe82..71c20a25a 100644 --- a/omnibus-base-mainnet-andromeda.toml +++ b/omnibus-base-mainnet-andromeda.toml @@ -1,5 +1,5 @@ name = "synthetix-omnibus" -version = "4" +version = "5" description = "Andromeda deployment" preset = "andromeda" include = [ @@ -97,3 +97,25 @@ args = [ [setting.usdc_address] defaultValue = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" + +[provision.sccp_313_spartan_council_pool_usdc_rewards] +source = "synthetix-rewards-distributor:0.0.2" +targetPreset = "<%= settings.target_preset %>" +options.salt = "<%= settings.salt %>" +options.rewardManager = "<%= imports.system.contracts.CoreProxy.address %>" +options.poolId = "<%= settings.sc_pool_id %>" +options.collateralType = "<%= extras.synth_usdc_token_address %>" +options.payoutToken = "<%= settings.usdc_address %>" +options.payoutTokenDecimals = "6" +options.name = "Spartan Council Pool USDC Rewards" + +[invoke.sccp_313_register_spartan_council_pool_usdc_rewards] +target = ["system.CoreProxy"] +fromCall.func = "getPoolOwner" +fromCall.args = ["<%= settings.sc_pool_id %>"] +func = "registerRewardsDistributor" +args = [ + "<%= settings.sc_pool_id %>", + "<%= extras.synth_usdc_token_address %>", + "<%= imports.sccp_313_spartan_council_pool_usdc_rewards.contracts.RewardsDistributor.address %>", +] diff --git a/omnibus-base-sepolia-andromeda.toml b/omnibus-base-sepolia-andromeda.toml index f257e3acf..f58dfa36f 100644 --- a/omnibus-base-sepolia-andromeda.toml +++ b/omnibus-base-sepolia-andromeda.toml @@ -1,5 +1,5 @@ name = "synthetix-omnibus" -version = "6" +version = "7" description = "Andromeda dev deployment" preset = "andromeda" include = [ @@ -117,3 +117,25 @@ args = [ "<%= extras.synth_usdc_token_address %>", "<%= imports.spartan_council_pool_rewards.contracts.RewardsDistributor.address %>", ] + +[provision.sccp_313_spartan_council_pool_usdc_rewards] +source = "synthetix-rewards-distributor:0.0.2" +targetPreset = "<%= settings.target_preset %>" +options.salt = "<%= settings.salt %>" +options.rewardManager = "<%= imports.system.contracts.CoreProxy.address %>" +options.poolId = "<%= settings.sc_pool_id %>" +options.collateralType = "<%= extras.synth_usdc_token_address %>" +options.payoutToken = "<%= imports.usdc_mock_collateral.contracts.MintableToken.address %>" +options.payoutTokenDecimals = "6" +options.name = "Spartan Council Pool USDC Rewards" + +[invoke.sccp_313_register_spartan_council_pool_usdc_rewards] +target = ["system.CoreProxy"] +fromCall.func = "getPoolOwner" +fromCall.args = ["<%= settings.sc_pool_id %>"] +func = "registerRewardsDistributor" +args = [ + "<%= settings.sc_pool_id %>", + "<%= extras.synth_usdc_token_address %>", + "<%= imports.sccp_313_spartan_council_pool_usdc_rewards.contracts.RewardsDistributor.address %>", +]