Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions markets/treasury-market/contracts/TreasuryMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,23 @@ contract TreasuryMarket is ITreasuryMarket, Ownable, UUPSImplementation, IMarket

// return the account token to the user. we use the "unsafe" call here because the account is being returned to the same address
// it came from, so its probably ok and the account will be able to handle receipt of the NFT.
// forge-lint: disable-next-line(erc20-unchecked-transfer)
accountToken.transferFrom(address(this), sender, accountId);

emit AccountUnsaddled(accountId, accountCollateral, neededToRepay);
}

function importStaker(
uint128 accountId,
uint256 saddledCollateralAmount,
LoanInfo memory loan,
AuxTokenInfo memory newAuxInfo
) external onlyOwner {
saddledCollateral[accountId] = saddledCollateralAmount;
loans[accountId] = loan;
auxTokenInfo[accountId] = newAuxInfo;
}

function reportAuxToken(uint128 accountId) external {
if (saddledCollateral[accountId] == 0 || loans[accountId].loanAmount == 0) {
return;
Expand Down Expand Up @@ -526,10 +538,19 @@ contract TreasuryMarket is ITreasuryMarket, Ownable, UUPSImplementation, IMarket
_upgradeTo(to);
}

function getLoanInfo(uint128 accountId) external view returns (LoanInfo memory) {
return loans[accountId];
}

function getAuxTokenInfo(uint128 accountId) external view returns (AuxTokenInfo memory) {
return auxTokenInfo[accountId];
}

/**
* @inheritdoc IERC721Receiver
* @dev This function is required so that self transfer in `unsaddle` works as expected.
*/
/// forge-lint: disable-next-line(mixed-case-function)
function onERC721Received(
address,
/*operator*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ interface ITreasuryMarket {
*/
error InsufficientExcessDebt(int256 neededToRepay, int256 ableToRepay);

/**
* @notice Emitted when `transferFrom` or `transfer` of an account token fails
*/
error AccountTokenTransferFailed(uint128 accountId);

/**
* @notice called by the owner to register this market with v3. This is an initialization call only.
*/
Expand Down
2 changes: 1 addition & 1 deletion markets/treasury-market/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@synthetixio/treasury-market",
"version": "3.13.0",
"version": "3.14.0",
"description": "V3 Market to allow a trusted entity to manage excess liquidity on behalf of stakers",
"publishConfig": {
"access": "public"
Expand Down
152 changes: 152 additions & 0 deletions markets/treasury-market/test/TreasuryMarket.test.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ interface IV3TestCoreProxy is IV3CoreProxy {}

/* solhint-disable numcast/safe-cast */

/// forge-lint: disable-start(all)

contract TreasuryMarketTest is Test, IERC721Receiver {
TreasuryMarket private market;
IV3TestCoreProxy private v3System;
Expand Down Expand Up @@ -1129,6 +1131,156 @@ contract TreasuryMarketTest is Test, IERC721Receiver {
assertEq(market.loanedAmount(accountId), initialDebt / 4);
}

function test_RevertIf_ImportStakerUnauthorized() external {
// Create test data
uint128 testAccountId = 100;
uint256 testSaddledCollateralAmount = 1000 ether;
ITreasuryMarket.LoanInfo memory testLoan = ITreasuryMarket.LoanInfo({
startTime: uint64(block.timestamp),
power: 2,
duration: 365 days,
loanAmount: 100 ether
});
ITreasuryMarket.AuxTokenInfo memory testAuxTokenInfo = ITreasuryMarket.AuxTokenInfo({
amount: 50 ether,
lastUpdated: uint64(block.timestamp),
timeInsufficient: 0,
epoch: 1
});

// Attempt to call importStaker as non-owner and expect revert
vm.expectRevert(abi.encodeWithSelector(AccessError.Unauthorized.selector, address(this)));
market.importStaker(testAccountId, testSaddledCollateralAmount, testLoan, testAuxTokenInfo);
}

function test_ImportStakerOverwritesExistingData() external {
// First, set some initial data
vm.warp(100000000);
uint128 testAccountId = 300;
uint256 initialSaddledAmount = 1000 ether;
ITreasuryMarket.LoanInfo memory initialLoan = ITreasuryMarket.LoanInfo({
startTime: uint64(block.timestamp - 1000),
power: 1,
duration: 365 days,
loanAmount: 50 ether
});
ITreasuryMarket.AuxTokenInfo memory initialAuxInfo = ITreasuryMarket.AuxTokenInfo({
amount: 25 ether,
lastUpdated: uint64(block.timestamp - 500),
timeInsufficient: 0,
epoch: 0
});

vm.prank(market.owner());
market.importStaker(testAccountId, initialSaddledAmount, initialLoan, initialAuxInfo);

// Now overwrite with new data
uint256 newSaddledAmount = 3000 ether;
ITreasuryMarket.LoanInfo memory newLoan = ITreasuryMarket.LoanInfo({
startTime: uint64(block.timestamp),
power: 4,
duration: 730 days,
loanAmount: 200 ether
});
ITreasuryMarket.AuxTokenInfo memory newAuxInfo = ITreasuryMarket.AuxTokenInfo({
amount: 100 ether,
lastUpdated: uint64(block.timestamp),
timeInsufficient: 172800, // 2 days
epoch: 3
});

vm.prank(market.owner());
market.importStaker(testAccountId, newSaddledAmount, newLoan, newAuxInfo);

// Verify the data was overwritten correctly
assertEq(
market.saddledCollateral(testAccountId),
newSaddledAmount,
"saddledCollateral not overwritten correctly"
);

assertEq(
market.getLoanInfo(testAccountId).startTime,
newLoan.startTime,
"loan startTime not overwritten correctly"
);
assertEq(
market.getLoanInfo(testAccountId).power,
newLoan.power,
"loan power not overwritten correctly"
);
assertEq(
market.getLoanInfo(testAccountId).duration,
newLoan.duration,
"loan duration not overwritten correctly"
);
assertEq(
market.getLoanInfo(testAccountId).loanAmount,
newLoan.loanAmount,
"loan loanAmount not overwritten correctly"
);

assertEq(
market.getAuxTokenInfo(testAccountId).amount,
newAuxInfo.amount,
"auxTokenInfo amount not overwritten correctly"
);
assertEq(
market.getAuxTokenInfo(testAccountId).lastUpdated,
newAuxInfo.lastUpdated,
"auxTokenInfo lastUpdated not overwritten correctly"
);
assertEq(
market.getAuxTokenInfo(testAccountId).timeInsufficient,
newAuxInfo.timeInsufficient,
"auxTokenInfo timeInsufficient not overwritten correctly"
);
assertEq(
market.getAuxTokenInfo(testAccountId).epoch,
newAuxInfo.epoch,
"auxTokenInfo epoch not overwritten correctly"
);
}

function test_ImportStakerWithZeroValues() external {
// Test with zero/minimal values to ensure no edge case issues
uint128 testAccountId = 400;
uint256 testSaddledCollateralAmount = 0;
ITreasuryMarket.LoanInfo memory testLoan = ITreasuryMarket.LoanInfo({
startTime: 0,
power: 0,
duration: 0,
loanAmount: 0
});
ITreasuryMarket.AuxTokenInfo memory testAuxTokenInfo = ITreasuryMarket.AuxTokenInfo({
amount: 0,
lastUpdated: 0,
timeInsufficient: 0,
epoch: 0
});

vm.prank(market.owner());
market.importStaker(testAccountId, testSaddledCollateralAmount, testLoan, testAuxTokenInfo);

// Verify all values are set to zero
assertEq(market.saddledCollateral(testAccountId), 0, "saddledCollateral should be 0");

(uint64 startTime, uint32 power, uint32 duration, uint128 loanAmount) = market.loans(
testAccountId
);
assertEq(startTime, 0, "loan startTime should be 0");
assertEq(power, 0, "loan power should be 0");
assertEq(duration, 0, "loan duration should be 0");
assertEq(loanAmount, 0, "loan loanAmount should be 0");

(uint128 amount, uint64 lastUpdated, uint32 timeInsufficient, uint32 epoch) = market
.auxTokenInfo(testAccountId);
assertEq(amount, 0, "auxTokenInfo amount should be 0");
assertEq(lastUpdated, 0, "auxTokenInfo lastUpdated should be 0");
assertEq(timeInsufficient, 0, "auxTokenInfo timeInsufficient should be 0");
assertEq(epoch, 0, "auxTokenInfo epoch should be 0");
}

function onERC721Received(
address,
/*operator*/
Expand Down
16 changes: 16 additions & 0 deletions protocol/synthetix/contracts/interfaces/IVaultModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ interface IVaultModule {
uint128 newPoolId
) external;

/**
* @notice Allows for the owner to override create a position (migrated from another network).
* @param accountId The id of the account associated with the position that will be updated.
* @param poolId The id of the pool associated with the position.
* @param collateralToken The address of the collateral used in the position.
* @param totalCollateral The total amount of collateral used in the position.
* @param totalDebt The total amount of debt used in the position.
*/
function importPosition(
uint128 accountId,
uint128 poolId,
address collateralToken,
uint256 totalCollateral,
int256 totalDebt
) external;

/**
* @notice Returns the collateralization ratio of the specified liquidity position. If debt is negative, this function will return 0.
* @dev Call this function using `callStatic` to treat it as a view function.
Expand Down
23 changes: 23 additions & 0 deletions protocol/synthetix/contracts/modules/core/VaultModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity >=0.8.11 <0.9.0;
import "@synthetixio/core-contracts/contracts/utils/DecimalMath.sol";
import "@synthetixio/core-contracts/contracts/utils/SafeCast.sol";
import "@synthetixio/core-contracts/contracts/utils/ERC2771Context.sol";
import "@synthetixio/core-contracts/contracts/ownership/OwnableStorage.sol";

import "../../storage/Account.sol";
import "../../storage/Pool.sol";
Expand Down Expand Up @@ -263,6 +264,28 @@ contract VaultModule is IVaultModule {
);
}

/**
* @inheritdoc IVaultModule
*/
function importPosition(
uint128 accountId,
uint128 poolId,
address collateralToken,
uint256 totalCollateral,
int256 totalDebt
) external {
OwnableStorage.onlyOwner();
Pool.load(poolId).vaults[collateralToken].currentEpoch().updateAccountPosition(
accountId,
totalCollateral,
1 ether
);
Pool.load(poolId).vaults[collateralToken].currentEpoch().assignDebtToAccount(
accountId,
totalDebt
);
}

/**
* @inheritdoc IVaultModule
*/
Expand Down
2 changes: 1 addition & 1 deletion protocol/synthetix/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@synthetixio/main",
"version": "3.13.0",
"version": "3.14.0",
"description": "Core Synthetix Protocol Contracts",
"publishConfig": {
"access": "public"
Expand Down